fix(core): 资产详情大图随数据显示,缩略图已出大图不再卡转圈

- 三视图/立绘大图改为跟数据(url)走,出图完成即显示,不再被在途轮询的
  busy 标志挡住——根治「小缩略图已出、大图一直转圈」
- 一并带上工作区其余改动(script_agent / services / tests / ai-tools / actor-library / App)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
zyc
2026-06-18 10:57:02 +08:00
co-authored by Claude Opus 4.8
parent fe38d3a512
commit 326c34ecd1
7 changed files with 200 additions and 47 deletions
+18 -13
View File
@@ -424,7 +424,7 @@ export function PipelinePage(props: {
scriptModelName: string;
textModels?: ModelConfig[]; onAdoptScript: (scriptId: string) => void | Promise<unknown>;
onUpdateShot: (payload: { segment_id: string; narration?: string; visual_prompt?: string; duration_seconds?: number }) => Promise<unknown>;
onAddShot: (afterSegmentId: string) => Promise<unknown>;
onAddShot: (afterSegmentId: string, content?: { narration?: string; visual_prompt?: string }) => Promise<unknown>;
onDeleteShot: (segmentId: string) => Promise<unknown>;
// 行35 · 单条分镜重跑(后端只重写该 segment);行28/34 · 把设定/标签合并进 project.metadata 持久化
onRerunShot?: (segmentId: string, instruction?: string) => Promise<unknown>;
@@ -650,7 +650,7 @@ export function PipelinePage(props: {
}
// 流程步骤4 · 演员库覆盖层:replace 带要替换的目标(已生成卡=groupId;seed 占位卡还没组=kind+label);
// browse 带 studio 直接进「添加人物工作台」
const [actorLib, setActorLib] = useState<{ mode: "browse" | "replace"; studio?: boolean; groupId?: string; seedKind?: "person" | "scene"; seedLabel?: string } | null>(null);
const [actorLib, setActorLib] = useState<{ mode: "browse" | "replace"; studio?: boolean; groupId?: string; seedKind?: "person" | "scene"; seedLabel?: string; addNew?: boolean } | null>(null);
function openActorReplace(entity: AssetEntity) {
setActorLib({ mode: "replace", groupId: entity.group.id });
}
@@ -658,9 +658,11 @@ export function PipelinePage(props: {
function openActorReplaceSeed(kind: "person" | "scene", tag: string) {
setActorLib({ mode: "replace", seedKind: kind, seedLabel: tag });
}
async function pickActor(assetId: string) {
async function pickActor(assetId: string, assetName?: string) {
if (actorLib?.mode === "replace") {
if (actorLib.groupId) await onAttachBaseAsset({ group_id: actorLib.groupId }, assetId);
// 新增人物(无既有 group / 无脚本标签):用所选演员名当 label 建一个新人物组挂上
else if (actorLib.addNew && actorLib.seedKind) await onAttachBaseAsset({ kind: actorLib.seedKind, label: (assetName || "").trim() }, assetId);
else if (actorLib.seedKind) await onAttachBaseAsset({ kind: actorLib.seedKind, label: actorLib.seedLabel }, assetId);
}
setActorLib(null);
@@ -2153,7 +2155,7 @@ export function PipelinePage(props: {
{/* 行34 · 该卡之后挂着的本地草稿分镜(点「添加分镜」插入,可编辑,有内容失焦才落库) */}
{draftShots.filter((d) => d.afterId === shot.id).map((draft) => (
<DraftShotCard key={draft.id} draft={draft}
onCommit={(d) => { setDraftShots((list) => list.filter((x) => x.id !== d.id)); if ((d.narration || d.visual).trim()) void onAddShot(d.afterId || shot.id); }}
onCommit={(d) => { setDraftShots((list) => list.filter((x) => x.id !== d.id)); if ((d.narration || d.visual).trim()) void onAddShot(d.afterId || shot.id, { narration: d.narration, visual_prompt: d.visual }); }}
onCancel={(id) => setDraftShots((list) => list.filter((x) => x.id !== id))} />
))}
<div className="shot-insert-gap">
@@ -2465,7 +2467,7 @@ export function PipelinePage(props: {
<h3>{KIND_LABEL[kind]} · {entities.length} 个</h3>
<span className="spacer"></span>
{/* 对齐设计稿:克制的小按钮(人物去演员库工作台;场景直接生成一版) */}
<button className="btn btn-sm" type="button" data-stop disabled={isBusy(customBusy)} onClick={() => { if (kind === "person") setActorLib({ mode: "browse", studio: true }); else setSceneDrafts((d) => [...d, { id: `scene-draft-${Date.now()}`, title: "", prompt: genPrompt }]); }}>{isBusy(customBusy) ? "生成中…" : `+ 新增${KIND_LABEL[kind]}`}</button>
<button className="btn btn-sm" type="button" data-stop disabled={isBusy(customBusy)} onClick={() => { if (kind === "person") setActorLib({ mode: "replace", seedKind: "person", addNew: true }); else setSceneDrafts((d) => [...d, { id: `scene-draft-${Date.now()}`, title: "", prompt: genPrompt }]); }}>{isBusy(customBusy) ? "生成中…" : `+ 新增${KIND_LABEL[kind]}`}</button>
</div>
<div className="asset-grid-2">
{/* 新增场景:本地占位草稿卡(可改标题 + 提示词),编辑完点「AI 生成」才出图;出图成功后移除草稿 */}
@@ -3279,12 +3281,13 @@ export function PipelinePage(props: {
{/* 左:大立绘 + 版本缩略 */}
<div className="asset-detail-lead">
<div className="ad-lead-wrap">
<div className={`placeholder ad-lead-img${portraitUrl && !busyPortrait ? " has-mock-media" : ""}`} style={portraitUrl && !busyPortrait ? mediaStyle(portraitUrl) : undefined}>
{busyPortrait
{/* 同三视图:大图随数据(portraitUrl)走,出图完成即显示,不被在途 busyPortrait 卡转圈 */}
<div className={`placeholder ad-lead-img${portraitUrl ? " has-mock-media" : ""}`} style={portraitUrl ? mediaStyle(portraitUrl) : undefined}>
{!portraitUrl && (busyPortrait
? <div style={{ display: "flex", flexDirection: "column", gap: 8, alignItems: "center" }}><span className="spinner" aria-hidden="true"></span><span className="ph-frame">立绘生成中…</span></div>
: !portraitUrl && <span className="ph-frame">立绘</span>}
: <span className="ph-frame">立绘</span>)}
</div>
{portraitUrl && !busyPortrait && <button className="ad-zoom-btn" type="button" title="查看大图" onClick={() => setPreview({ src: portraitUrl, kind: "image", name: `${entity.name} · 立绘` })}>{zoomSvg}</button>}
{portraitUrl && <button className="ad-zoom-btn" type="button" title="查看大图" onClick={() => setPreview({ src: portraitUrl, kind: "image", name: `${entity.name} · 立绘` })}>{zoomSvg}</button>}
</div>
{portraitVersions.length > 0 && (
<div className="ad-thumbs">
@@ -3311,12 +3314,14 @@ export function PipelinePage(props: {
</div>
<div className="asset-detail-tri-row">
<div className="ad-tri-wrap">
<div className={`placeholder${triUrl && !busyTri ? " has-mock-media" : ""}`} style={triUrl && !busyTri ? mediaStyle(triUrl) : undefined}>
{busyTri
{/* 大图随数据(triUrl)走,不被在途 poll 的 busyTri 挡住:出图完成(缩略图已有 url)即显示,
避免「缩略图已出、大图还在转圈」——busyTri 只在「还没任何结果」时显示生成中占位 */}
<div className={`placeholder${triUrl ? " has-mock-media" : ""}`} style={triUrl ? mediaStyle(triUrl) : undefined}>
{!triUrl && (busyTri
? <div style={{ display: "flex", flexDirection: "column", gap: 8, alignItems: "center" }}><span className="spinner" aria-hidden="true"></span><span className="ph-frame">三视图生成中…</span></div>
: !triUrl && <span className="ph-frame">正 / 侧 / 背 · 三视图</span>}
: <span className="ph-frame">正 / 侧 / 背 · 三视图</span>)}
</div>
{triUrl && !busyTri && <button className="ad-zoom-btn" type="button" title="查看大图" onClick={() => setPreview({ src: triUrl, kind: "image", name: `${entity.name} · 三视图` })}>{zoomSvg}</button>}
{triUrl && <button className="ad-zoom-btn" type="button" title="查看大图" onClick={() => setPreview({ src: triUrl, kind: "image", name: `${entity.name} · 三视图` })}>{zoomSvg}</button>}
</div>
</div>
{!triUrl && !busyTri && (