0fc180637c
通过 display:none 模式实现轻量 KeepAlive,电商页面首次访问后保持挂载, 切换到其他页面再切回时所有右侧面板状态(上传图片、生成进度、结果)完整保留。 同时清理项目中的临时文件和本地冗余图片。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
220 lines
8.4 KiB
TypeScript
220 lines
8.4 KiB
TypeScript
import { CloudUploadOutlined, LoadingOutlined, QuestionCircleOutlined } from "@ant-design/icons";
|
||
import type { ChangeEvent, RefObject } from "react";
|
||
import { EcommerceProgressBar } from "../EcommerceProgressBar";
|
||
|
||
interface EcommerceTryOnPanelProps {
|
||
garmentInputRef: RefObject<HTMLInputElement>;
|
||
garmentImages: Array<{ id: string; src: string; name: string }>;
|
||
modelSource: string;
|
||
modelGender: string;
|
||
modelAge: string;
|
||
modelEthnicity: string;
|
||
modelBody: string;
|
||
appearance: string;
|
||
selectedScenes: string[];
|
||
customScene: string;
|
||
smartScene: boolean;
|
||
tryOnRatio: string;
|
||
tryOnStatus: string;
|
||
canGenerateTryOn: boolean;
|
||
tryOnPrimaryLabel: string;
|
||
tryOnModelOptions: { gender: string[]; age: string[]; ethnicity: string[]; body: string[] };
|
||
tryOnAssets: { modelWoman: string; modelMan: string; modelAsian: string };
|
||
tryOnScenes: string[];
|
||
tryOnRatioOptions: string[];
|
||
handleGarmentUpload: (event: ChangeEvent<HTMLInputElement>) => void;
|
||
setModelSource: (value: "ai" | "library") => void;
|
||
setModelGender: (value: string) => void;
|
||
setModelAge: (value: string) => void;
|
||
setModelEthnicity: (value: string) => void;
|
||
setModelBody: (value: string) => void;
|
||
setAppearance: (value: string) => void;
|
||
handleGenerateModel: () => void;
|
||
toggleScene: (scene: string) => void;
|
||
setCustomScene: (value: string) => void;
|
||
setSmartScene: (updater: (current: boolean) => boolean) => void;
|
||
setTryOnRatio: (value: string) => void;
|
||
handleTryOnGenerate: () => void;
|
||
}
|
||
|
||
export default function EcommerceTryOnPanel({
|
||
garmentInputRef,
|
||
garmentImages,
|
||
modelSource,
|
||
modelGender,
|
||
modelAge,
|
||
modelEthnicity,
|
||
modelBody,
|
||
appearance,
|
||
selectedScenes,
|
||
customScene,
|
||
smartScene,
|
||
tryOnRatio,
|
||
tryOnStatus,
|
||
canGenerateTryOn,
|
||
tryOnPrimaryLabel,
|
||
tryOnModelOptions,
|
||
tryOnAssets,
|
||
tryOnScenes,
|
||
tryOnRatioOptions,
|
||
handleGarmentUpload,
|
||
setModelSource,
|
||
setModelGender,
|
||
setModelAge,
|
||
setModelEthnicity,
|
||
setModelBody,
|
||
setAppearance,
|
||
handleGenerateModel,
|
||
toggleScene,
|
||
setCustomScene,
|
||
setSmartScene,
|
||
setTryOnRatio,
|
||
handleTryOnGenerate,
|
||
}: EcommerceTryOnPanelProps) {
|
||
return (
|
||
<>
|
||
<div className="product-clone-panel__scroll">
|
||
<section className="product-clone-field">
|
||
<h2>服装图片</h2>
|
||
<button type="button" className="product-clone-upload-zone product-try-on-upload" onClick={() => garmentInputRef.current?.click()}>
|
||
<strong>
|
||
<CloudUploadOutlined />
|
||
服装图片
|
||
</strong>
|
||
<span>整套搭配或同一件服装不同角度图,最多5张。</span>
|
||
</button>
|
||
<input ref={garmentInputRef} type="file" accept="image/*" multiple onChange={handleGarmentUpload} />
|
||
{garmentImages.length ? (
|
||
<div className="product-clone-thumb-row product-try-on-thumb-row" aria-label="已上传服装图片">
|
||
{garmentImages.map((item) => (
|
||
<figure key={item.id} className="product-clone-uploaded-thumb">
|
||
<img src={item.src} alt={item.name} />
|
||
<span className="uploaded-image-zoom" aria-hidden="true">
|
||
<img src={item.src} alt="" />
|
||
</span>
|
||
</figure>
|
||
))}
|
||
</div>
|
||
) : null}
|
||
</section>
|
||
|
||
<section className="product-clone-field">
|
||
<h2>模特形象</h2>
|
||
<div className="product-clone-segment" role="tablist" aria-label="模特来源">
|
||
<button type="button" className={modelSource === "ai" ? "is-active" : ""} onClick={() => setModelSource("ai")}>
|
||
AI 生成
|
||
</button>
|
||
<button type="button" className={modelSource === "library" ? "is-active" : ""} onClick={() => setModelSource("library")}>
|
||
模特库
|
||
<QuestionCircleOutlined />
|
||
</button>
|
||
</div>
|
||
{modelSource === "ai" ? (
|
||
<>
|
||
<div className="product-clone-model-grid">
|
||
<select value={modelGender} onChange={(event) => setModelGender(event.target.value)}>
|
||
{tryOnModelOptions.gender.map((item) => (
|
||
<option key={item}>{item}</option>
|
||
))}
|
||
</select>
|
||
<select value={modelAge} onChange={(event) => setModelAge(event.target.value)}>
|
||
{tryOnModelOptions.age.map((item) => (
|
||
<option key={item}>{item}</option>
|
||
))}
|
||
</select>
|
||
<select value={modelEthnicity} onChange={(event) => setModelEthnicity(event.target.value)}>
|
||
{tryOnModelOptions.ethnicity.map((item) => (
|
||
<option key={item}>{item}</option>
|
||
))}
|
||
</select>
|
||
<select value={modelBody} onChange={(event) => setModelBody(event.target.value)}>
|
||
{tryOnModelOptions.body.map((item) => (
|
||
<option key={item}>{item}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
<label className="product-try-on-textarea-label">
|
||
<span>外貌细节(可选)</span>
|
||
<textarea
|
||
value={appearance}
|
||
onChange={(event) => setAppearance(event.target.value)}
|
||
placeholder="例如:小麦色皮肤、齐刘海、眼角有泪痣..."
|
||
/>
|
||
</label>
|
||
<button type="button" className="product-clone-model-button" onClick={handleGenerateModel} disabled={tryOnStatus === "modeling"}>
|
||
{tryOnStatus === "modeling" ? <LoadingOutlined /> : null}
|
||
{tryOnStatus === "modeling" ? "生成中..." : "生成基准模特"}
|
||
</button>
|
||
</>
|
||
) : (
|
||
<div className="product-try-on-library" aria-label="模特库">
|
||
{[tryOnAssets.modelWoman, tryOnAssets.modelMan, tryOnAssets.modelAsian].map((src, index) => (
|
||
<button key={src} type="button" className={index === 0 ? "is-active" : ""}>
|
||
<img src={src} alt={`模特 ${index + 1}`} />
|
||
</button>
|
||
))}
|
||
</div>
|
||
)}
|
||
</section>
|
||
|
||
<section className="product-clone-field">
|
||
<h2>拍摄场景</h2>
|
||
<div className="product-clone-scene-grid">
|
||
{tryOnScenes.map((scene) => (
|
||
<button
|
||
key={scene}
|
||
type="button"
|
||
className={selectedScenes.includes(scene) ? "is-active" : ""}
|
||
onClick={() => toggleScene(scene)}
|
||
>
|
||
<span aria-hidden="true" />
|
||
{scene}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</section>
|
||
|
||
<label className="product-clone-field product-try-on-scene-field">
|
||
<h2>或自定义描述场景(可选)</h2>
|
||
<textarea
|
||
value={customScene}
|
||
onChange={(event) => setCustomScene(event.target.value)}
|
||
placeholder="描述你想要的场景:如秋季枫叶小径、暖色调午后阳光、模特倚靠树干..."
|
||
/>
|
||
</label>
|
||
|
||
<section className="product-clone-field">
|
||
<button type="button" className="product-clone-switch-row" onClick={() => setSmartScene((current) => !current)}>
|
||
<span>
|
||
<strong>智能推荐场景</strong>
|
||
<em>根据服装自动匹配最佳场景</em>
|
||
</span>
|
||
<span className={`product-clone-switch${smartScene ? " is-on" : ""}`} role="switch" aria-checked={smartScene}>
|
||
<span />
|
||
</span>
|
||
</button>
|
||
</section>
|
||
|
||
<section className="product-clone-field">
|
||
<h2>图片比例</h2>
|
||
<div className="product-clone-ratio-row">
|
||
{tryOnRatioOptions.map((item) => (
|
||
<button key={item} type="button" className={tryOnRatio === item ? "is-active" : ""} onClick={() => setTryOnRatio(item)}>
|
||
{item}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</section>
|
||
</div>
|
||
|
||
<footer className="product-clone-panel__footer">
|
||
{tryOnStatus === "generating" ? <EcommerceProgressBar status="generating" label="服饰穿戴图" /> : null}
|
||
<button type="button" className="product-clone-primary" disabled={!canGenerateTryOn} onClick={handleTryOnGenerate}>
|
||
{tryOnStatus === "generating" ? <LoadingOutlined /> : null}
|
||
{tryOnPrimaryLabel}
|
||
</button>
|
||
</footer>
|
||
</>
|
||
);
|
||
}
|