Fix/ecommerce video 400 bug #11

Merged
stringadmin merged 11 commits from fix/ecommerce-video-400-bug into master 2026-06-04 08:05:21 +00:00
2 changed files with 88 additions and 42 deletions
Showing only changes of commit 31bf103d7c - Show all commits
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
CloseOutlined,
CopyOutlined,
DownloadOutlined,
FolderAddOutlined,
@@ -111,6 +112,7 @@ export default function EcommerceVideoWorkspace({
const [failedStep, setFailedStep] = useState<PlanStep | null>(null);
const [error, setError] = useState<string | null>(null);
const [actionNotice, setActionNotice] = useState<string | null>(null);
const [previewMedia, setPreviewMedia] = useState<{ url: string; type: "image" | "video" } | null>(null);
const abortControllerRef = useRef<AbortController | null>(null);
const renderAbortRef = useRef({ current: false });
const setView = useAppStore((s) => s.setView);
@@ -145,24 +147,17 @@ export default function EcommerceVideoWorkspace({
saveEcommerceVideoState({ inputFingerprint, stage, completedSteps, planResult, planProgress, scenes, sourceImageUrls });
}, [inputFingerprint, stage, completedSteps, planResult, planProgress, scenes, sourceImageUrls]);
// ── Auto-advance: skip manual "next step" clicks ─────────
const autoAdvanceTriggeredRef = useRef(false);
// ── Auto-advance: automatically run the full pipeline ─────────
useEffect(() => {
if (autoAdvanceTriggeredRef.current) return;
const delay = 600;
if (stage === "planned" && planResult && scenes.length > 0) {
autoAdvanceTriggeredRef.current = true;
const timer = setTimeout(() => { void handleGenerateImages(); }, delay);
return () => clearTimeout(timer);
}
if (stage === "imaged" && scenes.every((s) => s.imageUrl)) {
autoAdvanceTriggeredRef.current = true;
const timer = setTimeout(() => { void handleRenderVideos(); }, delay);
return () => clearTimeout(timer);
}
if (stage === "idle" || stage === "cancelled") {
autoAdvanceTriggeredRef.current = false;
}
}, [stage, scenes, planResult]);
// ── Keep-alive: resume polling for running tasks ──────────
@@ -638,11 +633,7 @@ export default function EcommerceVideoWorkspace({
{scenes.length > 0 ? scenes.map((s) => (
<div key={`trunk-${s.sceneId}`} className="ecom-video-tree__branch-tap" />
)) : (
<>
<div className="ecom-video-tree__branch-tap" />
<div className="ecom-video-tree__branch-tap" />
<div className="ecom-video-tree__branch-tap" />
</>
<div className="ecom-video-tree__branch-tap" />
)}
</div>
</div>
@@ -672,7 +663,7 @@ export default function EcommerceVideoWorkspace({
<svg viewBox="0 0 40 20" fill="none"><path d="M0 10 H28 M22 4 L30 10 L22 16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
</div>
<article className={`ecom-video-tree-node ecom-video-tree-node--image${imgReady ? " is-completed" : imgRunning ? " is-active" : ""}`}>
<article className={`ecom-video-tree-node ecom-video-tree-node--image${imgReady ? " is-completed" : imgRunning ? " is-active" : ""}`} onClick={imgReady ? () => setPreviewMedia({ url: scene.imageUrl!, type: "image" }) : undefined} style={imgReady ? { cursor: "pointer" } : undefined}>
{imgReady ? (
<img src={scene.imageUrl!} alt={`分镜${scene.sceneId}`} />
) : (
@@ -688,7 +679,7 @@ export default function EcommerceVideoWorkspace({
<svg viewBox="0 0 40 20" fill="none"><path d="M0 10 H28 M22 4 L30 10 L22 16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
</div>
<article className={`ecom-video-tree-node ecom-video-tree-node--video${vidReady ? " is-completed" : vidRunning ? " is-active" : vidFailed ? " is-failed" : ""}`}>
<article className={`ecom-video-tree-node ecom-video-tree-node--video${vidReady ? " is-completed" : vidRunning ? " is-active" : vidFailed ? " is-failed" : ""}`} onClick={vidReady ? () => setPreviewMedia({ url: scene.resultUrl!, type: "video" }) : undefined} style={vidReady ? { cursor: "pointer" } : undefined}>
{vidReady ? (
<video src={scene.resultUrl!} muted playsInline loop autoPlay />
) : (
@@ -709,34 +700,32 @@ export default function EcommerceVideoWorkspace({
</div>
);
}) : (
[1, 2, 3].map((n) => (
<div key={n} className={`ecom-video-tree__row ecom-video-tree__row--empty${stage === "planning" ? " is-planning" : ""}`} style={{ animationDelay: `${n * 120}ms` }}>
<article className="ecom-video-tree-node ecom-video-tree-node--text">
<div className="ecom-video-tree-node__inner">
<span className="ecom-video-tree-node__title">{n}</span>
<span className="ecom-video-tree-node__desc">{stage === "planning" ? "策划中..." : "等待策划"}</span>
</div>
</article>
<div className="ecom-video-tree__arrow" aria-hidden="true">
<svg viewBox="0 0 40 20" fill="none"><path d="M0 10 H28 M22 4 L30 10 L22 16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
<div className={`ecom-video-tree__row ecom-video-tree__row--empty${stage === "planning" ? " is-planning" : ""}`}>
<article className="ecom-video-tree-node ecom-video-tree-node--text">
<div className="ecom-video-tree-node__inner">
<span className="ecom-video-tree-node__title"></span>
<span className="ecom-video-tree-node__desc">{stage === "planning" ? "策划中..." : "点击一键策划开始"}</span>
</div>
<article className="ecom-video-tree-node ecom-video-tree-node--image">
<div className="ecom-video-tree-node__placeholder">
{stage === "planning" ? <LoadingOutlined /> : <span></span>}
</div>
<span className="ecom-video-tree-node__tag">{n}</span>
</article>
<div className="ecom-video-tree__arrow" aria-hidden="true">
<svg viewBox="0 0 40 20" fill="none"><path d="M0 10 H28 M22 4 L30 10 L22 16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
</div>
<article className="ecom-video-tree-node ecom-video-tree-node--video">
<div className="ecom-video-tree-node__placeholder">
{stage === "planning" ? <LoadingOutlined /> : <span></span>}
</div>
<span className="ecom-video-tree-node__tag">{n}</span>
</article>
</article>
<div className="ecom-video-tree__arrow" aria-hidden="true">
<svg viewBox="0 0 40 20" fill="none"><path d="M0 10 H28 M22 4 L30 10 L22 16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
</div>
))
<article className="ecom-video-tree-node ecom-video-tree-node--image">
<div className="ecom-video-tree-node__placeholder">
{stage === "planning" ? <LoadingOutlined /> : <span></span>}
</div>
<span className="ecom-video-tree-node__tag"></span>
</article>
<div className="ecom-video-tree__arrow" aria-hidden="true">
<svg viewBox="0 0 40 20" fill="none"><path d="M0 10 H28 M22 4 L30 10 L22 16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
</div>
<article className="ecom-video-tree-node ecom-video-tree-node--video">
<div className="ecom-video-tree-node__placeholder">
{stage === "planning" ? <LoadingOutlined /> : <span></span>}
</div>
<span className="ecom-video-tree-node__tag"></span>
</article>
</div>
)}
</div>
</div>
@@ -753,6 +742,19 @@ export default function EcommerceVideoWorkspace({
) : null}
{actionNotice ? <div className="ecom-video-flow-notice">{actionNotice}</div> : null}
</section>
{previewMedia ? (
<div className="ecom-video-preview-overlay" onClick={() => setPreviewMedia(null)}>
<button type="button" className="ecom-video-preview-overlay__close" onClick={() => setPreviewMedia(null)}>
<CloseOutlined />
</button>
{previewMedia.type === "image" ? (
<img src={previewMedia.url} alt="预览" onClick={(e) => e.stopPropagation()} />
) : (
<video src={previewMedia.url} controls autoPlay onClick={(e) => e.stopPropagation()} />
)}
</div>
) : null}
</div>
);
}
+44
View File
@@ -1101,3 +1101,47 @@
70% { opacity: 0.5; }
100% { opacity: 0; transform: translateX(100%); }
}
/* ── Preview lightbox overlay ────────────────────── */
.ecom-video-preview-overlay {
position: fixed;
inset: 0;
z-index: 9999;
display: grid;
place-items: center;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(8px);
cursor: zoom-out;
animation: ecom-preview-fade-in 200ms ease;
}
.ecom-video-preview-overlay__close {
position: absolute;
top: 24px;
right: 24px;
z-index: 10;
display: grid;
width: 40px;
height: 40px;
place-items: center;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 999px;
background: rgba(0, 0, 0, 0.6);
color: #fff;
font-size: 18px;
cursor: pointer;
}
.ecom-video-preview-overlay img,
.ecom-video-preview-overlay video {
max-width: 90vw;
max-height: 85vh;
border-radius: 8px;
object-fit: contain;
cursor: default;
}
@keyframes ecom-preview-fade-in {
from { opacity: 0; }
to { opacity: 1; }
}