Merge remote-tracking branch 'origin/main' into fix/ecommerce-ui-polish
# Conflicts: # src/features/ecommerce/EcommercePage.tsx
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user