Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 426e670934 | |||
| 5c07f0794a | |||
| ffd871490e | |||
| 7795ca3cbb | |||
| c70affc180 |
@@ -329,6 +329,20 @@ interface CanvasNode {
|
|||||||
y: number;
|
y: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RecordCanvasGroup {
|
||||||
|
id: string;
|
||||||
|
mode: string;
|
||||||
|
outputLabel: string;
|
||||||
|
settingLabel: string;
|
||||||
|
sourceImages: CloneImageItem[];
|
||||||
|
results: CloneResult[];
|
||||||
|
createdAt: number;
|
||||||
|
turnIndex: number;
|
||||||
|
status: EcommerceHistoryStatus;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface PreviewTouchPoint {
|
interface PreviewTouchPoint {
|
||||||
id: number;
|
id: number;
|
||||||
x: number;
|
x: number;
|
||||||
@@ -1847,9 +1861,19 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
{ key: "translate", label: "图片翻译", icon: <TranslationOutlined /> },
|
{ key: "translate", label: "图片翻译", icon: <TranslationOutlined /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
const renderQuickPageSidebar = (activeKey: NonNullable<typeof activeQuickTool>) => (
|
const commerceQuickPageSidebarItems = quickPageSidebarItems.filter((item) =>
|
||||||
|
["quick-set", "detail", "hot", "oneClickVideo"].includes(item.key),
|
||||||
|
);
|
||||||
|
const visualQuickPageSidebarItems = quickPageSidebarItems.filter((item) =>
|
||||||
|
["image-edit", "watermark", "copywriting", "translate"].includes(item.key),
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderQuickPageSidebar = (
|
||||||
|
activeKey: NonNullable<typeof activeQuickTool>,
|
||||||
|
group: "commerce" | "visual" = "commerce",
|
||||||
|
) => (
|
||||||
<nav className="ecom-quick-page-sidebar" aria-label="快捷工具切换">
|
<nav className="ecom-quick-page-sidebar" aria-label="快捷工具切换">
|
||||||
{quickPageSidebarItems.map((item) => (
|
{(group === "visual" ? visualQuickPageSidebarItems : commerceQuickPageSidebarItems).map((item) => (
|
||||||
<button
|
<button
|
||||||
key={item.key}
|
key={item.key}
|
||||||
type="button"
|
type="button"
|
||||||
@@ -4843,6 +4867,65 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
isOneClickVideoTool ||
|
isOneClickVideoTool ||
|
||||||
isVideoWorkspaceVisible ||
|
isVideoWorkspaceVisible ||
|
||||||
Boolean(activeHistoryRecordId);
|
Boolean(activeHistoryRecordId);
|
||||||
|
const isMainCloneWorkspace = isCloneTool && !isSmartCutoutTool && !isQuickDetailTool && !isWatermarkTool && !isTranslateTool && !isImageEditTool && !isQuickSetTool && !isCopywritingTool && !isOneClickVideoTool;
|
||||||
|
const isRecordDetailWorkspace = isMainCloneWorkspace && Boolean(activeHistoryRecordId);
|
||||||
|
const activeHistoryRecord = activeHistoryRecordId ? ecommerceHistoryRecords.find((record) => record.id === activeHistoryRecordId) : null;
|
||||||
|
const activeConversationTurns = activeHistoryRecord
|
||||||
|
? activeHistoryRecord.turns?.length
|
||||||
|
? activeHistoryRecord.turns
|
||||||
|
: [buildHistoryTurnFromRecord(activeHistoryRecord)]
|
||||||
|
: [];
|
||||||
|
const activeRecallTurn = activeConversationTurns[activeConversationTurns.length - 1];
|
||||||
|
const commandRecallPrompt =
|
||||||
|
requirement.trim() ||
|
||||||
|
activeRecallTurn?.requirement?.trim() ||
|
||||||
|
activeHistoryRecord?.requirement?.trim() ||
|
||||||
|
"展开完整对话,继续补充生成需求。";
|
||||||
|
const commandRecallAsset = productImages[0] || activeRecallTurn?.productImages?.[0] || activeHistoryRecord?.productImages?.[0];
|
||||||
|
const commandRecallStatus =
|
||||||
|
status === "generating"
|
||||||
|
? "生成中"
|
||||||
|
: activeRecallTurn?.status === "failed" || status === "failed"
|
||||||
|
? "可恢复"
|
||||||
|
: "继续生成";
|
||||||
|
const hasGeneratedCloneWork = status === "done" || canvasNodes.length > 0;
|
||||||
|
const shouldUseCompactComposer = isCommandComposerCompact && hasGeneratedCloneWork && !(isRecordDetailWorkspace && !isCloneConversationCollapsed);
|
||||||
|
const shouldShowScenarioScrollHint = !isRecordDetailWorkspace;
|
||||||
|
const getHistoryTurnSettingLabel = (turn: EcommerceHistoryTurn) => {
|
||||||
|
if (turn.settingLabel) return turn.settingLabel;
|
||||||
|
if (turn.output === "set" && turn.results?.length && !turn.setResultImages?.length) {
|
||||||
|
return `单图 ${turn.results.length}张`;
|
||||||
|
}
|
||||||
|
if (turn.output === "set") {
|
||||||
|
const total = cloneSetCountKeys.reduce((sum, key) => sum + (turn.setCounts?.[key] ?? 0), 0);
|
||||||
|
return `套图 ${total || 1}张`;
|
||||||
|
}
|
||||||
|
if (turn.output === "detail") return `详情 ${turn.detailModules?.length || 1}项`;
|
||||||
|
if (turn.output === "model") return `模特 ${turn.modelScenes?.length || 1}景`;
|
||||||
|
return cloneOutputOptions.find((option) => option.key === turn.output)?.label || selectedCloneOutput.label;
|
||||||
|
};
|
||||||
|
const recordCanvasGroups: RecordCanvasGroup[] = isRecordDetailWorkspace
|
||||||
|
? activeConversationTurns.reduce<RecordCanvasGroup[]>((groups, turn, index) => {
|
||||||
|
const turnResults = getTurnResults(turn);
|
||||||
|
if (!turnResults.length && turn.status !== "generating") return groups;
|
||||||
|
const canvasNode = canvasNodes.find((node) => node.id === turn.id);
|
||||||
|
const outputLabel = turn.modeLabel || cloneOutputOptions.find((option) => option.key === turn.output)?.label || selectedCloneOutput.label;
|
||||||
|
groups.push({
|
||||||
|
id: turn.id,
|
||||||
|
mode: turn.output,
|
||||||
|
outputLabel,
|
||||||
|
settingLabel: getHistoryTurnSettingLabel(turn),
|
||||||
|
sourceImages: turn.productImages,
|
||||||
|
results: turnResults,
|
||||||
|
createdAt: turn.createdAt,
|
||||||
|
turnIndex: index + 1,
|
||||||
|
status: turn.status,
|
||||||
|
x: canvasNode?.x ?? groups.length * 420,
|
||||||
|
y: canvasNode?.y ?? (groups.length % 2 === 0 ? 0 : 160),
|
||||||
|
});
|
||||||
|
return groups;
|
||||||
|
}, [])
|
||||||
|
: [];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onWorkspaceChromeChange?.({ isToolPage: isWorkspaceToolPage });
|
onWorkspaceChromeChange?.({ isToolPage: isWorkspaceToolPage });
|
||||||
@@ -6293,8 +6376,125 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{status === "done" || canvasNodes.length > 0 ? (
|
{status === "done" || canvasNodes.length > 0 || (isRecordDetailWorkspace && recordCanvasGroups.length > 0) ? (
|
||||||
<div className="clone-ai-preview-zoom-wrap" style={previewTransformStyle}>
|
<div className="clone-ai-preview-zoom-wrap" style={previewTransformStyle}>
|
||||||
|
{isRecordDetailWorkspace ? (
|
||||||
|
<section className="clone-ai-turn-groups" aria-label="按轮次生成结果">
|
||||||
|
{recordCanvasGroups.map((group) => {
|
||||||
|
const primarySource = group.sourceImages[0];
|
||||||
|
const dragNode: CanvasNode = {
|
||||||
|
id: group.id,
|
||||||
|
mode: group.mode,
|
||||||
|
sourceImage: primarySource?.src,
|
||||||
|
results: group.results,
|
||||||
|
createdAt: group.createdAt,
|
||||||
|
x: group.x,
|
||||||
|
y: group.y,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<article
|
||||||
|
key={group.id}
|
||||||
|
className={`clone-ai-canvas-node clone-ai-turn-group is-${group.status}`}
|
||||||
|
data-mode={group.mode}
|
||||||
|
data-turn-id={group.id}
|
||||||
|
data-node-id={group.id}
|
||||||
|
style={{ transform: `translate(${group.x}px, ${group.y}px)` }}
|
||||||
|
onPointerDown={(event) => startCanvasNodeDrag(event, dragNode)}
|
||||||
|
onPointerMove={(event) => moveCanvasNodeDrag(event, group.id)}
|
||||||
|
onPointerUp={(event) => stopCanvasNodeDrag(event, group.id)}
|
||||||
|
onPointerCancel={(event) => stopCanvasNodeDrag(event, group.id)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="clone-ai-node-drag-handle"
|
||||||
|
onPointerDown={(e) => {
|
||||||
|
if (e.button !== 0) return;
|
||||||
|
e.stopPropagation();
|
||||||
|
e.currentTarget.setPointerCapture(e.pointerId);
|
||||||
|
nodeDragRef.current = { active: true, nodeId: group.id, startX: e.clientX, startY: e.clientY, originX: group.x, originY: group.y };
|
||||||
|
}}
|
||||||
|
onPointerMove={(e) => {
|
||||||
|
const drag = nodeDragRef.current;
|
||||||
|
if (!drag.active || drag.nodeId !== group.id) return;
|
||||||
|
const zoom = previewZoomRef.current;
|
||||||
|
const dx = (e.clientX - drag.startX) / zoom;
|
||||||
|
const dy = (e.clientY - drag.startY) / zoom;
|
||||||
|
setCanvasNodes((prev) => prev.map((n) => n.id === group.id ? { ...n, x: drag.originX + dx, y: drag.originY + dy } : n));
|
||||||
|
}}
|
||||||
|
onPointerUp={(e) => {
|
||||||
|
if (nodeDragRef.current.nodeId === group.id) {
|
||||||
|
nodeDragRef.current = { ...nodeDragRef.current, active: false };
|
||||||
|
e.currentTarget.releasePointerCapture(e.pointerId);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className={`clone-ai-turn-group__body${primarySource?.src ? "" : " is-without-source"}`}>
|
||||||
|
{primarySource?.src ? (
|
||||||
|
<>
|
||||||
|
<div className="clone-ai-source-stack">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="clone-ai-source-corner-action"
|
||||||
|
onClick={() => openProductSetPreview({ src: primarySource.src, label: "原图素材" })}
|
||||||
|
>
|
||||||
|
原图素材
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="clone-ai-main-result"
|
||||||
|
aria-label="预览原图素材"
|
||||||
|
onClick={() => openProductSetPreview({ src: primarySource.src, label: "原图素材" })}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={primarySource.src}
|
||||||
|
alt={primarySource.name || "原图素材"}
|
||||||
|
onError={(event) => {
|
||||||
|
event.currentTarget.style.display = "none";
|
||||||
|
event.currentTarget.parentElement?.classList.add("is-missing-source");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="clone-ai-source-missing">素材不可用</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="clone-ai-flow-arrow" aria-hidden="true" />
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
<div className="clone-ai-result-stack">
|
||||||
|
<span className="clone-ai-node-label">{group.outputLabel}</span>
|
||||||
|
<div className="clone-ai-result-grid result-reveal">
|
||||||
|
{group.results.map((card) => (
|
||||||
|
<button key={card.id} type="button" style={{ aspectRatio: parseRatioToAspectCss(ratio) }} onClick={() => openProductSetPreview(card, { nodeId: group.id, removable: true })}>
|
||||||
|
<img src={card.src} alt={card.label} />
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
{group.status === "generating" && !group.results.length ? (
|
||||||
|
<div className="clone-ai-turn-group__pending" aria-live="polite">
|
||||||
|
<LoadingOutlined />
|
||||||
|
<span>正在生成{group.outputLabel}...</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{status === "generating" && !recordCanvasGroups.some((group) => group.status === "generating") ? (
|
||||||
|
<article className="clone-ai-turn-group is-generating">
|
||||||
|
<header className="clone-ai-turn-group__head">
|
||||||
|
<div>
|
||||||
|
<span>继续生成</span>
|
||||||
|
<strong>{selectedCloneOutput.label}</strong>
|
||||||
|
</div>
|
||||||
|
<small>AI 正在整理新一轮结果</small>
|
||||||
|
</header>
|
||||||
|
<div className="clone-ai-turn-group__pending" aria-live="polite">
|
||||||
|
<LoadingOutlined />
|
||||||
|
<span>正在生成{selectedCloneOutput.label}...</span>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
) : null}
|
||||||
|
</section>
|
||||||
|
) : (
|
||||||
<section className="clone-ai-canvas-nodes" aria-label="生成结果">
|
<section className="clone-ai-canvas-nodes" aria-label="生成结果">
|
||||||
{canvasNodes.map((node) => (
|
{canvasNodes.map((node) => (
|
||||||
<article
|
<article
|
||||||
@@ -6380,6 +6580,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
</article>
|
</article>
|
||||||
) : null}
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : status === "idle" || status === "ready" ? null : (
|
) : status === "idle" || status === "ready" ? null : (
|
||||||
<section className="clone-ai-empty-state" aria-live="polite">
|
<section className="clone-ai-empty-state" aria-live="polite">
|
||||||
@@ -6405,13 +6606,71 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
|
|
||||||
<section
|
<section
|
||||||
ref={commandComposerWrapRef}
|
ref={commandComposerWrapRef}
|
||||||
className={`clone-ai-bottom-input ecom-command-composer-wrap${status === "done" || canvasNodes.length > 0 ? " has-generated" : " is-before-generate"}${isCommandComposerCompact && (status === "done" || canvasNodes.length > 0) ? " is-compact" : ""}`}
|
className={`clone-ai-bottom-input ecom-command-composer-wrap${hasGeneratedCloneWork ? " has-generated" : " is-before-generate"}${shouldUseCompactComposer ? " is-compact" : ""}${isRecordDetailWorkspace && isCloneConversationCollapsed ? " is-recall-entry" : ""}`}
|
||||||
aria-label="生成指令"
|
aria-label={isRecordDetailWorkspace && isCloneConversationCollapsed ? "展开完整生成指令" : "生成指令"}
|
||||||
|
role={isRecordDetailWorkspace && isCloneConversationCollapsed ? "button" : undefined}
|
||||||
|
tabIndex={isRecordDetailWorkspace && isCloneConversationCollapsed ? 0 : undefined}
|
||||||
|
onClickCapture={(event) => {
|
||||||
|
if (isRecordDetailWorkspace && isCloneConversationCollapsed) {
|
||||||
|
if ((event.target as HTMLElement).closest(".ecom-command-recall__home")) return;
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
setIsCloneConversationCollapsed(false);
|
||||||
|
window.setTimeout(() => requirementTextareaRef.current?.focus(), 180);
|
||||||
|
}
|
||||||
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
if (isRecordDetailWorkspace && isCloneConversationCollapsed) {
|
||||||
|
setIsCloneConversationCollapsed(false);
|
||||||
|
window.setTimeout(() => requirementTextareaRef.current?.focus(), 180);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (isCommandComposerCompact) setIsCommandComposerCompact(false);
|
if (isCommandComposerCompact) setIsCommandComposerCompact(false);
|
||||||
}}
|
}}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (!isRecordDetailWorkspace || !isCloneConversationCollapsed) return;
|
||||||
|
if (event.key !== "Enter" && event.key !== " ") return;
|
||||||
|
event.preventDefault();
|
||||||
|
setIsCloneConversationCollapsed(false);
|
||||||
|
window.setTimeout(() => requirementTextareaRef.current?.focus(), 180);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<h1 className={`ecom-command-title${status === "done" || canvasNodes.length > 0 ? " is-after-generate" : ""}`}>
|
{isRecordDetailWorkspace && isCloneConversationCollapsed ? (
|
||||||
|
<div className="ecom-command-recall">
|
||||||
|
<span className="ecom-command-recall__media" aria-hidden="true">
|
||||||
|
{commandRecallAsset ? (
|
||||||
|
<img src={commandRecallAsset.src} alt="" />
|
||||||
|
) : (
|
||||||
|
<PaperClipOutlined />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span className="ecom-command-recall__content">
|
||||||
|
<span className="ecom-command-recall__eyebrow">
|
||||||
|
<em>{commandRecallStatus}</em>
|
||||||
|
</span>
|
||||||
|
<span className="ecom-command-recall__text">{commandRecallPrompt}</span>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ecom-command-recall__home"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
handleNewEcommerceConversation();
|
||||||
|
}}
|
||||||
|
aria-label="返回首页"
|
||||||
|
title="返回首页"
|
||||||
|
>
|
||||||
|
<AppstoreOutlined />
|
||||||
|
<span>首页</span>
|
||||||
|
</button>
|
||||||
|
<span className="ecom-command-recall__action" aria-hidden="true">
|
||||||
|
<span>继续</span>
|
||||||
|
<MenuUnfoldOutlined />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<h1 className={`ecom-command-title${hasGeneratedCloneWork ? " is-after-generate" : ""}`}>
|
||||||
{typewriterText}
|
{typewriterText}
|
||||||
<span className="typewriter-cursor" aria-hidden="true">|</span>
|
<span className="typewriter-cursor" aria-hidden="true">|</span>
|
||||||
</h1>
|
</h1>
|
||||||
@@ -7586,7 +7845,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className={`ecom-quick-set-upload${detailProductImages.length ? " has-images" : ""}`}
|
className={`ecom-quick-set-upload ecom-quick-hot-material${detailProductImages.length ? " has-images" : ""}`}
|
||||||
onClick={() => detailInputRef.current?.click()}
|
onClick={() => detailInputRef.current?.click()}
|
||||||
onKeyDown={(event) => openQuickUploadWithKeyboard(event, detailInputRef)}
|
onKeyDown={(event) => openQuickUploadWithKeyboard(event, detailInputRef)}
|
||||||
onDragOver={(event) => event.preventDefault()}
|
onDragOver={(event) => event.preventDefault()}
|
||||||
@@ -7596,7 +7855,24 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
<span>拖拽或点击上传</span>
|
<span>拖拽或点击上传</span>
|
||||||
<em>同一产品,最多 3 张</em>
|
<em>同一产品,最多 3 张</em>
|
||||||
<b>+ 上传图片</b>
|
<b>+ 上传图片</b>
|
||||||
{detailProductImages.length ? renderQuickUploadThumbs(detailProductImages, removeDetailImage) : null}
|
{detailProductImages.length ? (
|
||||||
|
<>
|
||||||
|
{renderHotMaterialThumbs(detailProductImages, removeDetailImage)}
|
||||||
|
{detailProductImages.length < 3 ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ecom-quick-hot-add-btn"
|
||||||
|
aria-label="Add more detail images"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
detailInputRef.current?.click();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlusOutlined />
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
ref={detailInputRef}
|
ref={detailInputRef}
|
||||||
@@ -7682,6 +7958,17 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
<button type="button" className="ecom-quick-set-primary ecom-quick-set-primary--cancel" onClick={handleCancelGenerate}>取消生成</button>
|
<button type="button" className="ecom-quick-set-primary ecom-quick-set-primary--cancel" onClick={handleCancelGenerate}>取消生成</button>
|
||||||
) : null}
|
) : null}
|
||||||
</aside>
|
</aside>
|
||||||
|
{hotMaterialHoverZoom && typeof document !== "undefined"
|
||||||
|
? createPortal(
|
||||||
|
<div
|
||||||
|
className={`ecom-hot-material-zoom-portal is-${hotMaterialHoverZoom.placement}`}
|
||||||
|
style={{ left: hotMaterialHoverZoom.x, top: hotMaterialHoverZoom.y }}
|
||||||
|
>
|
||||||
|
<img src={hotMaterialHoverZoom.src} alt="" />
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
)
|
||||||
|
: null}
|
||||||
<section className="ecom-quick-set-stage">
|
<section className="ecom-quick-set-stage">
|
||||||
<header className="ecom-quick-set-preview-head">
|
<header className="ecom-quick-set-preview-head">
|
||||||
<h1>预览</h1>
|
<h1>预览</h1>
|
||||||
@@ -8050,7 +8337,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
onDragLeave={(event) => { event.preventDefault(); event.stopPropagation(); if (event.currentTarget === event.target || !event.currentTarget.contains(event.relatedTarget as Node)) setIsProductUploadDragging(false); }}
|
onDragLeave={(event) => { event.preventDefault(); event.stopPropagation(); if (event.currentTarget === event.target || !event.currentTarget.contains(event.relatedTarget as Node)) setIsProductUploadDragging(false); }}
|
||||||
onDrop={(event) => { event.preventDefault(); event.stopPropagation(); setIsProductUploadDragging(false); const files = Array.from(event.dataTransfer.files); if (files.length) addProductImages(files); }}
|
onDrop={(event) => { event.preventDefault(); event.stopPropagation(); setIsProductUploadDragging(false); const files = Array.from(event.dataTransfer.files); if (files.length) addProductImages(files); }}
|
||||||
>
|
>
|
||||||
{renderQuickUploadThumbs(productImages, removeProductImage)}
|
{renderHotMaterialThumbs(productImages, removeProductImage)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="ecom-quick-hot-add-btn"
|
className="ecom-quick-hot-add-btn"
|
||||||
@@ -8209,6 +8496,17 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
{hotMaterialHoverZoom && typeof document !== "undefined"
|
||||||
|
? createPortal(
|
||||||
|
<div
|
||||||
|
className={`ecom-hot-material-zoom-portal is-${hotMaterialHoverZoom.placement}`}
|
||||||
|
style={{ left: hotMaterialHoverZoom.x, top: hotMaterialHoverZoom.y }}
|
||||||
|
>
|
||||||
|
<img src={hotMaterialHoverZoom.src} alt="" />
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
)
|
||||||
|
: null}
|
||||||
<section className="ecom-quick-set-stage">
|
<section className="ecom-quick-set-stage">
|
||||||
<header className="ecom-quick-set-preview-head">
|
<header className="ecom-quick-set-preview-head">
|
||||||
<h1>预览</h1>
|
<h1>预览</h1>
|
||||||
@@ -8383,11 +8681,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|
||||||
const copywritingPreview = (
|
const copywritingPreview = <EcommerceCopywritingPanel onClose={closeCopywritingPage} />;
|
||||||
<div key="copywriting" className="ecom-quick-page-wrap ecom-tool-page-enter">
|
|
||||||
<EcommerceCopywritingPanel onClose={closeCopywritingPage} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const oneClickVideoPreview = (
|
const oneClickVideoPreview = (
|
||||||
<div key="oneClickVideo" className="ecom-quick-page-wrap ecom-tool-page-enter">
|
<div key="oneClickVideo" className="ecom-quick-page-wrap ecom-tool-page-enter">
|
||||||
@@ -8435,21 +8729,21 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
? isWatermarkTool
|
? isWatermarkTool
|
||||||
? (
|
? (
|
||||||
<div key={`quick-${activeQuickTool}`} className="ecom-quick-page-wrap ecom-tool-page-enter">
|
<div key={`quick-${activeQuickTool}`} className="ecom-quick-page-wrap ecom-tool-page-enter">
|
||||||
{renderQuickPageSidebar("watermark")}
|
{renderQuickPageSidebar("watermark", "visual")}
|
||||||
{watermarkPreview}
|
{watermarkPreview}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
: isTranslateTool
|
: isTranslateTool
|
||||||
? (
|
? (
|
||||||
<div key={`quick-${activeQuickTool}`} className="ecom-quick-page-wrap ecom-tool-page-enter">
|
<div key={`quick-${activeQuickTool}`} className="ecom-quick-page-wrap ecom-tool-page-enter">
|
||||||
{renderQuickPageSidebar("translate")}
|
{renderQuickPageSidebar("translate", "visual")}
|
||||||
{translatePreview}
|
{translatePreview}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
: isImageEditTool
|
: isImageEditTool
|
||||||
? (
|
? (
|
||||||
<div key={`quick-${activeQuickTool}`} className="ecom-quick-page-wrap ecom-tool-page-enter">
|
<div key={`quick-${activeQuickTool}`} className="ecom-quick-page-wrap ecom-tool-page-enter">
|
||||||
{renderQuickPageSidebar("image-edit")}
|
{renderQuickPageSidebar("image-edit", "visual")}
|
||||||
{imageWorkbenchPreview}
|
{imageWorkbenchPreview}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -8479,41 +8773,20 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
: isCopywritingTool
|
: isCopywritingTool
|
||||||
? (
|
? (
|
||||||
<div key={`quick-${activeQuickTool}`} className="ecom-quick-page-wrap ecom-tool-page-enter">
|
<div key={`quick-${activeQuickTool}`} className="ecom-quick-page-wrap ecom-tool-page-enter">
|
||||||
{renderQuickPageSidebar("copywriting")}
|
{renderQuickPageSidebar("copywriting", "visual")}
|
||||||
{copywritingPreview}
|
{copywritingPreview}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
: isOneClickVideoTool
|
: isOneClickVideoTool
|
||||||
? (
|
? (
|
||||||
<div key={`quick-${activeQuickTool}`} className="ecom-quick-page-wrap ecom-tool-page-enter">
|
<div key={`quick-${activeQuickTool}`} className="ecom-quick-page-wrap ecom-one-click-video-wrap ecom-tool-page-enter">
|
||||||
{renderQuickPageSidebar("oneClickVideo")}
|
{renderQuickPageSidebar("oneClickVideo")}
|
||||||
{oneClickVideoPreview}
|
{oneClickVideoPreview}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
: clonePreview
|
: clonePreview
|
||||||
: placeholderPreview;
|
: placeholderPreview;
|
||||||
const isMainCloneWorkspace = isCloneTool && !isSmartCutoutTool && !isQuickDetailTool && !isWatermarkTool && !isTranslateTool && !isImageEditTool && !isQuickSetTool && !isCopywritingTool && !isOneClickVideoTool;
|
|
||||||
const isRecordDetailWorkspace = isMainCloneWorkspace && Boolean(activeHistoryRecordId);
|
|
||||||
const currentResultCount = canvasNodes.reduce((count, node) => count + node.results.length, 0);
|
const currentResultCount = canvasNodes.reduce((count, node) => count + node.results.length, 0);
|
||||||
const activeHistoryRecord = activeHistoryRecordId ? ecommerceHistoryRecords.find((record) => record.id === activeHistoryRecordId) : null;
|
|
||||||
const activeConversationTurns = activeHistoryRecord
|
|
||||||
? activeHistoryRecord.turns?.length
|
|
||||||
? activeHistoryRecord.turns
|
|
||||||
: [buildHistoryTurnFromRecord(activeHistoryRecord)]
|
|
||||||
: [];
|
|
||||||
const getHistoryTurnSettingLabel = (turn: EcommerceHistoryTurn) => {
|
|
||||||
if (turn.settingLabel) return turn.settingLabel;
|
|
||||||
if (turn.output === "set" && turn.results?.length && !turn.setResultImages?.length) {
|
|
||||||
return `单图 ${turn.results.length}张`;
|
|
||||||
}
|
|
||||||
if (turn.output === "set") {
|
|
||||||
const total = cloneSetCountKeys.reduce((sum, key) => sum + (turn.setCounts?.[key] ?? 0), 0);
|
|
||||||
return `套图 ${total || 1}张`;
|
|
||||||
}
|
|
||||||
if (turn.output === "detail") return `详情 ${turn.detailModules?.length || 1}项`;
|
|
||||||
if (turn.output === "model") return `模特 ${turn.modelScenes?.length || 1}景`;
|
|
||||||
return cloneOutputOptions.find((option) => option.key === turn.output)?.label || selectedCloneOutput.label;
|
|
||||||
};
|
|
||||||
const restoreHistoryTurnInputs = (turn: EcommerceHistoryTurn) => {
|
const restoreHistoryTurnInputs = (turn: EcommerceHistoryTurn) => {
|
||||||
setCloneOutput(turn.output);
|
setCloneOutput(turn.output);
|
||||||
setPlatform(turn.platform);
|
setPlatform(turn.platform);
|
||||||
@@ -8574,18 +8847,30 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
<>
|
<>
|
||||||
<aside className="clone-ai-conversation-panel" aria-label="AI 对话">
|
<aside className="clone-ai-conversation-panel" aria-label="AI 对话">
|
||||||
<header className="clone-ai-conversation-head">
|
<header className="clone-ai-conversation-head">
|
||||||
<div>
|
<div className="clone-ai-conversation-title">
|
||||||
<strong>{activeHistoryRecord?.title || "生成详情"}</strong>
|
<strong>{activeHistoryRecord?.title || "生成详情"}</strong>
|
||||||
<span>{selectedCloneOutput.label} · {platform} · {language}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="clone-ai-conversation-actions">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
className="clone-ai-conversation-home"
|
||||||
|
onClick={handleNewEcommerceConversation}
|
||||||
|
aria-label="返回首页"
|
||||||
|
title="返回首页"
|
||||||
|
>
|
||||||
|
<AppstoreOutlined />
|
||||||
|
<span>首页</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="clone-ai-conversation-collapse"
|
||||||
onClick={() => setIsCloneConversationCollapsed(true)}
|
onClick={() => setIsCloneConversationCollapsed(true)}
|
||||||
aria-label="收起对话"
|
aria-label="收起对话"
|
||||||
title="收起对话"
|
title="收起对话"
|
||||||
>
|
>
|
||||||
<MenuFoldOutlined />
|
<MenuFoldOutlined />
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div className="clone-ai-conversation-body">
|
<div className="clone-ai-conversation-body">
|
||||||
{activeConversationTurns.map((turn, index) => {
|
{activeConversationTurns.map((turn, index) => {
|
||||||
@@ -8657,16 +8942,6 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="clone-ai-conversation-toggle"
|
|
||||||
onClick={() => setIsCloneConversationCollapsed((current) => !current)}
|
|
||||||
aria-label={isCloneConversationCollapsed ? "展开对话" : "收起对话"}
|
|
||||||
title={isCloneConversationCollapsed ? "展开对话" : "收起对话"}
|
|
||||||
aria-expanded={!isCloneConversationCollapsed}
|
|
||||||
>
|
|
||||||
{isCloneConversationCollapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
|
||||||
</button>
|
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export default function CommandHistorySidebar({
|
|||||||
</div>
|
</div>
|
||||||
<div className="ecom-command-history__heading">
|
<div className="ecom-command-history__heading">
|
||||||
<strong>生成记录</strong>
|
<strong>生成记录</strong>
|
||||||
<span>{records.length} 条</span>
|
<span className="ecom-command-history__count">{records.length} 条</span>
|
||||||
</div>
|
</div>
|
||||||
{refreshMessage ? (
|
{refreshMessage ? (
|
||||||
<p key={refreshStamp} className="ecom-command-history__refresh-note" role="status">
|
<p key={refreshStamp} className="ecom-command-history__refresh-note" role="status">
|
||||||
@@ -86,13 +86,25 @@ export default function CommandHistorySidebar({
|
|||||||
{records.length ? (
|
{records.length ? (
|
||||||
records.map((record) => {
|
records.map((record) => {
|
||||||
const outputLabel = outputLabels.find((option) => option.key === record.output)?.label || "生成记录";
|
const outputLabel = outputLabels.find((option) => option.key === record.output)?.label || "生成记录";
|
||||||
|
const statusKey = record.status === "generating" ? "generating" : record.status === "failed" ? "failed" : "done";
|
||||||
const statusLabel =
|
const statusLabel =
|
||||||
record.status === "generating" ? "生成中" : record.status === "failed" ? "失败" : formatHistoryTime(record.createdAt);
|
record.status === "generating" ? "生成中" : record.status === "failed" ? "失败" : formatHistoryTime(record.createdAt);
|
||||||
return (
|
return (
|
||||||
<div key={`${record.id}-${refreshTick}`} className={`ecom-command-history__item${activeRecordId === record.id ? " is-active" : ""}`}>
|
<div
|
||||||
<button type="button" className="ecom-command-history__item-main" onClick={() => onOpenRecord(record)}>
|
key={`${record.id}-${refreshTick}`}
|
||||||
|
className={`ecom-command-history__item is-${statusKey}${activeRecordId === record.id ? " is-active" : ""}`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`ecom-command-history__item-main${activeRecordId === record.id ? " is-active" : ""}`}
|
||||||
|
onClick={() => onOpenRecord(record)}
|
||||||
|
aria-current={activeRecordId === record.id ? "page" : undefined}
|
||||||
|
>
|
||||||
<strong>{record.title}</strong>
|
<strong>{record.title}</strong>
|
||||||
<span>{outputLabel} · {statusLabel}</span>
|
<span className="ecom-command-history__item-meta">
|
||||||
|
<span>{outputLabel}</span>
|
||||||
|
<em>{statusLabel}</em>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user