From 38f4d95951659d9a99e3c191bc69da05702839e2 Mon Sep 17 00:00:00 2001 From: seaislee1209 Date: Tue, 23 Jun 2026 16:57:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(video):=20=E8=A7=86=E9=A2=91=E5=8D=A1?= =?UTF-8?q?=E4=BA=A4=E4=BA=92=E5=8F=8D=E9=A6=88=20+=20=E3=80=8C=E5=85=A8?= =?UTF-8?q?=E9=83=A8=E9=87=8D=E8=B7=91=E3=80=8D=E7=9C=9F=E9=87=8D=E8=B7=91?= =?UTF-8?q?=E5=B7=B2=E5=AE=8C=E6=88=90=E7=9A=84=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 单卡「重跑」/ 详情弹窗「重跑本场」/「全部重跑」点下立刻进「提交中」态:按钮转圈、视频块叠 spinner 遮罩,撑到该段真进 running(busy 接管),堵掉空窗连点 → 防双倍扣费、不再点完没反应 - 「全部重跑」改为把已完成(succeeded)的段也重跑(原来过滤掉 succeeded → 全成功项目点了等于 没点、还弹假的「已提交」);只跳过在途(running/queued)避免重复提交 - 视频卡加「共 N 版 ›」小标识(N>1 才显示),点开详情弹窗看历史版本/切换/采用 Co-Authored-By: Claude Opus 4.8 (1M context) --- core/frontend/src/App.tsx | 3 +- core/frontend/src/pipeline-page.css | 6 +++ core/frontend/src/routes/pipeline.tsx | 55 +++++++++++++++++++++++---- 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/core/frontend/src/App.tsx b/core/frontend/src/App.tsx index a075454..c1ac3e4 100644 --- a/core/frontend/src/App.tsx +++ b/core/frontend/src/App.tsx @@ -921,7 +921,8 @@ export function App() { onSubmitVideo={(segmentId, prompt) => action(() => api.submitVideo(pipelineProject.id, { video_segment_id: segmentId, prompt }), "视频片段已提交,生成中…")} onSubmitAllVideos={(prompt) => action(async () => { - const targets = pipelineProject.video_segments.filter((segment) => !["running", "succeeded"].includes(segment.status)); + // 「全部重跑」要把已完成的也重跑(否则全成功的项目点了等于没点);只跳过在途(running/queued)避免重复提交 + const targets = pipelineProject.video_segments.filter((segment) => !["running", "queued"].includes(segment.status)); for (const segment of targets) { await api.submitVideo(pipelineProject.id, { video_segment_id: segment.id, diff --git a/core/frontend/src/pipeline-page.css b/core/frontend/src/pipeline-page.css index 89c63cc..c197086 100644 --- a/core/frontend/src/pipeline-page.css +++ b/core/frontend/src/pipeline-page.css @@ -466,6 +466,12 @@ .video-card-head .pill { flex: 0 0 auto; } .video-meta { font-family: var(--font-mono); font-size: 12px; color: var(--black-alpha-48); letter-spacing: .02em; margin-top: 5px; } .video-actions { margin-top: auto; padding-top: 12px; display: flex; align-items: center; gap: 10px; } + /* 按钮内联小 spinner(重跑「提交中/生成中」转圈),配合文字;在 btn-primary 上转白色可见 */ + .btn-spin { width: 12px; height: 12px; border-width: 2px; flex: 0 0 auto; margin-right: 5px; vertical-align: -1px; } + .btn-primary .btn-spin { border-color: color-mix(in srgb, currentColor 35%, transparent); border-top-color: currentColor; } + /* 多版本入口小标识「共 N 版 ›」—— 不是按钮、是可点 mono 标识,hover 转主橙 */ + .video-ver-badge { background: none; border: 0; padding: 0; cursor: pointer; font-family: var(--font-mono); font-size: 12px; letter-spacing: .02em; color: var(--black-alpha-48); transition: color var(--t-base); } + .video-ver-badge:hover { color: var(--heat); } /* ============= STAGE 5 · 拼接编辑器 ============= */ .content--fh .stage[data-stage-pane="5"].active > .editor { flex: 1 1 0; min-height: 0; overflow-y: auto; height: auto; } diff --git a/core/frontend/src/routes/pipeline.tsx b/core/frontend/src/routes/pipeline.tsx index 468640f..61a4b08 100644 --- a/core/frontend/src/routes/pipeline.tsx +++ b/core/frontend/src/routes/pipeline.tsx @@ -483,8 +483,8 @@ export function PipelinePage(props: { onRenameActor?: (assetId: string, name: string) => Promise; onGenerateStoryboard: (prompt: string) => void | Promise; onSkipStoryboard: () => Promise; - onSubmitVideo: (segmentId: string, prompt: string) => void; - onSubmitAllVideos: (prompt: string) => void; + onSubmitVideo: (segmentId: string, prompt: string) => void | Promise; + onSubmitAllVideos: (prompt: string) => void | Promise; onPollVideosQuiet: () => void | Promise; exportResult: ExportPoll | null; onRefreshExport: () => void; @@ -874,6 +874,38 @@ export function PipelinePage(props: { setVdVerId(null); setVdPrompt(""); } + // 重跑「提交中」乐观态:点下立刻禁用+转圈,撑到该段真的进 running(busy 接管),堵掉空窗连点 → 防双倍扣费 + const [rerunPending, setRerunPending] = useState>(() => new Set()); + const clearRerunPending = (segId: string) => + setRerunPending((s) => { if (!s.has(segId)) return s; const n = new Set(s); n.delete(segId); return n; }); + function submitVideoOptimistic(segId: string, prompt: string) { + setRerunPending((s) => new Set(s).add(segId)); + Promise.resolve(onSubmitVideo(segId, prompt)) + .then((res) => { if (res == null) clearRerunPending(segId); }) // 失败(action 返回 null)→ 立即解禁可重试;成功交给下方 effect + .catch(() => clearRerunPending(segId)); + window.setTimeout(() => clearRerunPending(segId), 20000); // 兜底:异常下也别永久禁用 + } + // 「全部重跑」乐观态:把将被提交的段(非在途)全部立刻标 pending → 每张卡马上转圈,不必等状态回传 + function submitAllVideosOptimistic() { + const ids = segments.filter((s) => !["running", "queued"].includes(s.status)).map((s) => s.id); + if (ids.length === 0) return; + setRerunPending((s) => { const n = new Set(s); ids.forEach((id) => n.add(id)); return n; }); + Promise.resolve(onSubmitAllVideos(videoPrompt)) + .then((res) => { if (res == null) ids.forEach(clearRerunPending); }) // 失败 → 解禁;成功交给上面的 effect 逐个摘除 + .catch(() => ids.forEach(clearRerunPending)); + ids.forEach((id) => window.setTimeout(() => clearRerunPending(id), 20000)); + } + // 段状态变化时,把已真正进入 running/queued 的段从 pending 摘掉(此后由 busy 驱动 UI) + const segStatusKey = segments.map((s) => `${s.id}:${s.status}`).join("|"); + useEffect(() => { + setRerunPending((prev) => { + if (prev.size === 0) return prev; + const next = new Set(prev); + for (const seg of segments) if (next.has(seg.id) && ["running", "queued"].includes(seg.status)) next.delete(seg.id); + return next.size === prev.size ? prev : next; + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [segStatusKey]); // Esc 关闭详情弹窗 useEffect(() => { if (!vdSegId) return; @@ -3085,7 +3117,7 @@ export function PipelinePage(props: {
{pct}% - + + + {/* 多版本入口:N>1 才显示,点开详情弹窗看历史/切换/采用(数据全留着,不吞历史) */} + {verCount > 1 && ( + + )} {/* AI 生成片段不需单卡上传(自定义替换走 queue-bar 全局上传);移除多余「上传」按钮 */} {url @@ -3524,6 +3564,7 @@ export function PipelinePage(props: { {/* ── Stage 4 · 视频详情弹窗:大预览 + 历史版本(可切/可采用) + 本场提示词 + 重跑 ── */} {vdSeg && (() => { const busy = ["running", "queued"].includes(vdSeg.status); + const showBusy = busy || rerunPending.has(vdSeg.id); const hint = (shots[vdSeg.sort_order]?.visual_prompt || shots[vdSeg.sort_order]?.narration || "").trim(); const rerunPrompt = vdPrompt.trim() || `${videoPrompt} 第 ${vdSeg.sort_order + 1} 段,时长 ${vdSeg.target_duration_seconds} 秒`; return ( @@ -3589,8 +3630,8 @@ export function PipelinePage(props: {
{vdVer?.asset_url && 下载} -