feat(storyboard): 整张生成时每张分镜转圈(不全屏遮挡)+ 未生成占位回 2:3 竖屏

- 每张分镜占位(缩略图 + 大图)叠半透明遮罩 + spinner,样式同基础资产趴;不再全屏遮挡
- 中间大图空占位由 16:9 横框改 2:3 竖屏,与成品(1024×1536)同形;fh-flat 下加兜底高度居中
- 生成结束(成功/失败)spinner 自动停

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
seaislee1209
2026-06-23 13:16:16 +08:00
co-authored by Claude Opus 4.8
parent 9c2a3bc365
commit dbd1382fb1
2 changed files with 15 additions and 5 deletions
+8 -1
View File
@@ -385,6 +385,8 @@
.content--fh-flat .stage[data-stage-pane="3"].active > .stage-storyboard > .sb-canvas > .sb-main-img { width: 100%; }
/* 有真图时大图占满画布整个高度(不再 16/9 居中留上下空白) */
.content--fh-flat .stage[data-stage-pane="3"].active > .stage-storyboard > .sb-canvas > .sb-main-img.has-mock-media { align-self: stretch; aspect-ratio: auto; }
/* 2:3 空占位:别被上面的 width:100% 撑成超高满框 —— 给个合理高度、按 2:3 推宽、居中 */
.content--fh-flat .stage[data-stage-pane="3"].active > .stage-storyboard > .sb-canvas > .sb-main-img:not(.has-mock-media) { width: auto; height: min(70vh, 540px); aspect-ratio: 2/3; justify-self: center; }
.content--fh-flat .stage[data-stage-pane="3"].active > .stage-storyboard > .sb-side { display: flex; flex-direction: column; min-height: 0; }
.content--fh-flat .stage[data-stage-pane="3"].active > .stage-storyboard > .sb-side > .pane { flex: 1 1 0; min-height: 0; overflow-y: auto; border: 0; border-radius: 0; background: var(--surface); padding: 18px 28px; }
.content--fh-flat .stage[data-stage-pane="3"].active .sb-scenes-col { max-height: none; }
@@ -400,10 +402,15 @@
.sb-scene-thumb .placeholder { aspect-ratio: 1; }
.sb-scene-thumb .nm { font-size: 11.5px; font-weight: 500; color: var(--accent-black); }
.sb-scene-thumb .sub { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); }
.sb-main-img { aspect-ratio: 16/9; min-height: 0; }
/* 整张故事板出图是竖屏 2:3(1024×1536),空占位也用 2:3,跟成品同形(不再 16:9 横框) */
.sb-main-img { aspect-ratio: 2/3; min-height: 0; }
/* 整张故事板是竖长拼图,cover 会裁掉上下 —— 用 contain 完整显示;
留白底色用白(--surface)而非占位浅灰,避免「灰底大框」(对齐 HTML 干净白底) */
.sb-main-img.has-mock-media { background-size: contain; background-color: var(--surface); }
/* 整张生成中:每张分镜占位上叠一层半透明遮罩 + 转圈 spinner(不全屏遮挡;样式同基础资产趴) */
.sb-gen-veil { position: absolute; inset: 0; z-index: 3; display: grid; place-items: center; border-radius: inherit; pointer-events: none; background: color-mix(in srgb, var(--surface) 62%, transparent); }
.sb-scene-thumb .sb-gen-veil .spinner { width: 16px; height: 16px; }
.sb-main-img .sb-gen-veil .spinner { width: 28px; height: 28px; }
.sb-rerun-note { display: flex; align-items: flex-start; gap: 10px; padding: 10px 12px; margin-bottom: 14px; background: rgba(180,83,9,.08); border: 1px solid rgba(180,83,9,.20); border-radius: var(--r-md); color: #7C3A05; line-height: 1.55; }
.sb-rerun-note .warn-ic { width: 22px; height: 22px; border-radius: var(--r-sm); background: rgba(180,83,9,.12); color: #B45309; display: grid; place-items: center; flex: 0 0 22px; }
+7 -4
View File
@@ -481,7 +481,7 @@ export function PipelinePage(props: {
onGenerateTriview: (portraitGroupId: string) => Promise<{ id: string } | null>;
// 流程步骤4 · 添加人物工作台命名:把名字写回该人物资产
onRenameActor?: (assetId: string, name: string) => Promise<unknown>;
onGenerateStoryboard: (prompt: string) => void;
onGenerateStoryboard: (prompt: string) => void | Promise<unknown>;
onSkipStoryboard: () => Promise<unknown>;
onSubmitVideo: (segmentId: string, prompt: string) => void;
onSubmitAllVideos: (prompt: string) => void;
@@ -846,6 +846,8 @@ export function PipelinePage(props: {
// 让用户一眼看出「这里本该有几张」,出图后逐个填真图。无采用版脚本则为 0(显示「暂无」)。
const sbAdoptedScript = scripts.find((s) => s.is_adopted) || null;
const sbExpectedShots = sbAdoptedScript ? (sbAdoptedScript.segments?.length ?? 0) : 0;
// 整张故事板生成中:不全屏遮挡,只在每张分镜占位上转个 spinner(样式同基础资产趴)
const [sbGenerating, setSbGenerating] = useState(false);
// 没生成故事板时点「确认故事板」→ 弹气泡提示先去生成(3.5s 自动收起)
const [sbConfirmHint, setSbConfirmHint] = useState(false);
useEffect(() => {
@@ -2943,7 +2945,7 @@ export function PipelinePage(props: {
const url = frameUrl(frame);
return (
<div className={`sb-scene-thumb${idx === sbSelected ? " selected" : ""}`} key={frame.id} data-sid={frame.id} onClick={() => setSbSelected(idx)}>
<div className={`placeholder${url ? " has-mock-media" : ""}`} style={url ? mediaStyle(url) : undefined}><span className="ph-frame"> {idx + 1}</span></div>
<div className={`placeholder${url ? " has-mock-media" : ""}`} style={url ? mediaStyle(url) : undefined}><span className="ph-frame"> {idx + 1}</span>{sbGenerating && <span className="sb-gen-veil" aria-hidden="true"><span className="spinner" /></span>}</div>
<div className="nm"> {idx + 1}</div>
<div className="sub">{times[idx] || `#${frame.sort_order + 1}`}</div>
</div>
@@ -2953,7 +2955,7 @@ export function PipelinePage(props: {
/* 还没出图:按采用版脚本镜头数铺等量空占位,标「待生成」,让用户看出本该有几张 */
Array.from({ length: sbExpectedShots }, (_, idx) => (
<div className={`sb-scene-thumb${idx === sbSelected ? " selected" : ""}`} key={`ph-${idx}`} onClick={() => setSbSelected(idx)}>
<div className="placeholder"><span className="ph-frame"> {idx + 1}</span></div>
<div className="placeholder"><span className="ph-frame"> {idx + 1}</span>{sbGenerating && <span className="sb-gen-veil" aria-hidden="true"><span className="spinner" /></span>}</div>
<div className="nm"> {idx + 1}</div>
<div className="sub">// 待生成</div>
</div>
@@ -2965,6 +2967,7 @@ export function PipelinePage(props: {
return (
<div className={`placeholder sb-main-img${url ? " has-mock-media" : ""}`} id="sb-main-img" style={url ? { ...mediaStyle(url), cursor: "zoom-in" } : undefined} role={url ? "button" : undefined} tabIndex={url ? 0 : undefined} title={url ? "点击放大" : undefined} onClick={url ? () => setPreview({ src: url, kind: "image", name: `${sbSelected + 1}` }) : undefined} onKeyDown={url ? (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); setPreview({ src: url, kind: "image", name: `${sbSelected + 1}` }); } } : undefined}>
<span className="ph-frame">{sbActiveFrame ? `${sbSelected + 1}` : sbExpectedShots ? `${sbSelected + 1} · 待生成` : "// 故事板未生成"}</span>
{sbGenerating && <span className="sb-gen-veil" aria-hidden="true"><span className="spinner" /></span>}
</div>
);
})()}
@@ -2999,7 +3002,7 @@ export function PipelinePage(props: {
onChange={(v) => setStoryboardPrompt(v.trim())}
/>
<div className="sb-stage-actions">
<button className="pill-cta heat" type="button" id="sb-rerun-btn" disabled={loading} onClick={() => guardGen(shots, () => onGenerateStoryboard(storyboardPrompt || SB_PROMPT_DEFAULT))}>
<button className="pill-cta heat" type="button" id="sb-rerun-btn" disabled={loading} onClick={() => guardGen(shots, () => { setSbGenerating(true); void Promise.resolve(onGenerateStoryboard(storyboardPrompt || SB_PROMPT_DEFAULT)).finally(() => setSbGenerating(false)); })}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><path d="M4 12a8 8 0 0 1 14-5.5L21 9" /><path d="M21 4v5h-5" /><path d="M20 12a8 8 0 0 1-14 5.5L3 15" /><path d="M3 20v-5h5" /></svg>
{adoptedStoryboard ? "整张重跑" : "生成故事板"}
</button>