Merge remote-tracking branch 'origin/main' into fix/ecommerce-ui-polish

# Conflicts:
#	src/features/ecommerce/EcommercePage.tsx
This commit is contained in:
2026-06-12 16:04:09 +08:00
3 changed files with 832 additions and 101 deletions
+71 -39
View File
@@ -4,6 +4,7 @@
CloudUploadOutlined,
CloseOutlined,
DeleteOutlined,
DownloadOutlined,
EditOutlined,
FireOutlined,
FileImageOutlined,
@@ -38,6 +39,7 @@ import EcommerceSetPanel from "./panels/EcommerceSetPanel";
import EcommerceTryOnPanel from "./panels/EcommerceTryOnPanel";
import EcommerceClonePanel from "./panels/EcommerceClonePanel";
import { ecommerceOssScopes, saveUnifiedEcommerceGenerationRecord, deleteEcommerceGenerationRecord } from "./ecommerceGenerationPersistence";
import { downloadResultAsset } from "../workbench/workbenchDownload";
const smartCutoutColorPresets = [
"#ffffff",
@@ -349,6 +351,14 @@ interface EcommerceHistoryRecord {
replicateLevel: CloneReplicateLevelKey;
}
interface ProductSetPreviewSelection {
src: string;
label: string;
nodeId?: string;
cardId?: string;
removable?: boolean;
}
interface EcommerceImagePromptOptions {
gender?: string;
age?: string;
@@ -1377,7 +1387,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
const [productSetStatus, setProductSetStatus] = useState<ProductSetStatus>("idle");
const [productSetResultImages, setProductSetResultImages] = useState<string[]>([]);
const [isSetUploadDragging, setIsSetUploadDragging] = useState(false);
const [selectedProductSetPreview, setSelectedProductSetPreview] = useState<{ src: string; label: string } | null>(null);
const [selectedProductSetPreview, setSelectedProductSetPreview] = useState<ProductSetPreviewSelection | null>(null);
const [showHostingModal, setShowHostingModal] = useState(false);
const [productImages, setProductImages] = useState<CloneImageItem[]>([]);
const [activeQuickTool, setActiveQuickTool] = useState<"cutout" | "detail" | "watermark" | "image-edit" | "translate" | null>(null);
@@ -3836,8 +3846,39 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
lastFailedActionRef.current = () => handleSetGenerate();
};
const openProductSetPreview = (card: { src: string; label: string }) => {
setSelectedProductSetPreview(card);
const openProductSetPreview = (card: { id?: string; src: string; label: string }, options?: { nodeId?: string; removable?: boolean }) => {
setSelectedProductSetPreview({
src: card.src,
label: card.label,
cardId: card.id,
nodeId: options?.nodeId,
removable: Boolean(options?.removable && options.nodeId && card.id),
});
};
const handleDownloadCanvasResult = async (card: { src: string; label: string }) => {
try {
await downloadResultAsset(card.src, card.label || "generated-image", false);
toast.success("已开始下载图片");
} catch (error) {
toast.error(error instanceof Error ? error.message : "下载图片失败");
}
};
const removeCanvasResult = (nodeId: string, cardId: string) => {
setCanvasNodes((current) =>
current
.map((node) => (node.id === nodeId ? { ...node, results: node.results.filter((card) => card.id !== cardId) } : node))
.filter((node) => node.sourceImage || node.results.length > 0),
);
setResults((current) => current.filter((card) => card.id !== cardId));
toast.success("已从当前视图移除");
};
const removeSelectedProductSetPreview = (preview: ProductSetPreviewSelection) => {
if (!preview.nodeId || !preview.cardId) return;
removeCanvasResult(preview.nodeId, preview.cardId);
setSelectedProductSetPreview(null);
};
const handleDetailAiWrite = () => {
@@ -4141,24 +4182,6 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
setPreviewOffset({ x: 0, y: 0 });
previewOffsetRef.current = { x: 0, y: 0 };
requestAnimationFrame(() => requestAnimationFrame(() => {
const container = previewSurfaceRef.current;
if (!container) return;
const containerRect = container.getBoundingClientRect();
const node = container.querySelector(".clone-ai-canvas-node") as HTMLElement | null;
if (!node) return;
const nodeRect = node.getBoundingClientRect();
const nodeCenterY = nodeRect.top + nodeRect.height / 2 - containerRect.top;
const visibleCenterY = containerRect.height / 2;
const offsetY = visibleCenterY - nodeCenterY;
const nodeCenterX = nodeRect.left + nodeRect.width / 2 - containerRect.left;
const visibleCenterX = containerRect.width / 2;
const offsetX = visibleCenterX - nodeCenterX;
const finalOffset = { x: offsetX, y: offsetY };
setPreviewOffset(finalOffset);
previewOffsetRef.current = finalOffset;
}));
};
const handleNewEcommerceConversation = () => {
@@ -4982,7 +5005,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
<div className="clone-ai-flow-arrow" aria-hidden="true" />
<div className="clone-ai-result-grid result-reveal">
{node.results.map((card) => (
<button key={card.id} type="button" style={{ aspectRatio: parseRatioToAspectCss(ratio) }} onClick={() => openProductSetPreview(card)}>
<button key={card.id} type="button" style={{ aspectRatio: parseRatioToAspectCss(ratio) }} onClick={() => openProductSetPreview(card, { nodeId: node.id, removable: true })}>
<img src={card.src} alt={card.label} />
<span>{card.label}</span>
</button>
@@ -6499,7 +6522,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
return (
<section
className={`product-clone-page page-motion${isCloneTool && isCloneSettingsCollapsed ? " is-settings-collapsed" : ""}${isCloneTool && isCommandHistoryCollapsed ? " is-history-collapsed" : ""}${isSmartCutoutTool ? " is-smart-cutout-page" : ""}${isQuickDetailTool ? " is-quick-set-page" : ""}${isWatermarkTool ? " is-watermark-page" : ""}${isTranslateTool ? " is-translate-page" : ""}${isImageEditTool ? " is-image-workbench-page" : ""}`}
className={`product-clone-page page-motion${isCloneTool && isCloneSettingsCollapsed ? " is-settings-collapsed" : ""}${isCloneTool && isCommandHistoryCollapsed ? " is-history-collapsed" : ""}${isCloneTool && activeHistoryRecordId ? " is-history-detail" : ""}${isSmartCutoutTool ? " is-smart-cutout-page" : ""}${isQuickDetailTool ? " is-quick-set-page" : ""}${isWatermarkTool ? " is-watermark-page" : ""}${isTranslateTool ? " is-translate-page" : ""}${isImageEditTool ? " is-image-workbench-page" : ""}`}
data-tool={activeTool}
aria-label={pageLabel}
>
@@ -6623,22 +6646,31 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
<CloseOutlined />
</button>
<img src={selectedProductSetPreview.src} alt={selectedProductSetPreview.label} />
<strong>{selectedProductSetPreview.label}</strong>
<button
type="button"
className="product-set-preview-download"
onClick={() => {
const link = document.createElement("a");
link.href = selectedProductSetPreview.src;
link.download = `${selectedProductSetPreview.label || "生成结果"}-${Date.now()}.png`;
document.body.appendChild(link);
link.click();
link.remove();
}}
>
<CloudUploadOutlined />
</button>
<div className="product-set-preview-footer">
<strong>{selectedProductSetPreview.label}</strong>
<div className="product-set-preview-actions" aria-label="图片操作">
<button
type="button"
className="product-set-preview-action"
onClick={() => {
void handleDownloadCanvasResult(selectedProductSetPreview);
}}
>
<DownloadOutlined />
<span></span>
</button>
{selectedProductSetPreview.removable ? (
<button
type="button"
className="product-set-preview-action product-set-preview-action--danger"
onClick={() => removeSelectedProductSetPreview(selectedProductSetPreview)}
>
<DeleteOutlined />
<span></span>
</button>
) : null}
</div>
</div>
</section>
</div>
) : null}