Files
omniai-web/src/features/ecommerce/panels/EcommerceTryOnPanel.tsx
T
stringadmin 0fc180637c feat: 电商页面 KeepAlive 保活机制,切换页面不再丢失生成状态
通过 display:none 模式实现轻量 KeepAlive,电商页面首次访问后保持挂载,
切换到其他页面再切回时所有右侧面板状态(上传图片、生成进度、结果)完整保留。
同时清理项目中的临时文件和本地冗余图片。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 23:20:57 +08:00

220 lines
8.4 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, 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>
</>
);
}