fix(core): 基础资产「生成中」占位卡刷新后从后端认领恢复(出图本在 worker 跑,只是前端 loading 丢了)

问题:立绘/商品图出图是异步(worker),但 loading 占位只活在前端内存 genBusy + 当前页轮询 Promise,
刷新即丢 → 占位卡退回「待生成」,虽 worker 仍在跑、跑完手动刷新才见。
修复:
- 后端 GET /api/projects/{id}/pending-assets/ 返回在途出图任务(id/kind/label/status,读 request_payload)
- 前端进基础资产趴轮询该端点,据 (kind,label) 用 pendingHas 把对应占位卡置「生成中」;
  在途数减少=有任务完成 → onRefreshProject 把占位卡换成真卡。离开该趴停止轮询。
tsc + py_compile + 21 测试通过。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
zyc
2026-06-17 15:36:30 +08:00
co-authored by Claude Opus 4.8
parent 296888f0bf
commit 0d1fce1cc1
3 changed files with 58 additions and 4 deletions
+6
View File
@@ -207,6 +207,12 @@ export const api = {
project(id: string) {
return request<Project>(`/api/projects/${id}/`);
},
// 本项目在途的基础资产出图任务(刷新后据此重建「生成中」占位卡 loading)
pendingAssets(id: string) {
return request<{ pending: Array<{ id: string; kind: string; label: string; is_triview: boolean; status: string }> }>(
`/api/projects/${id}/pending-assets/`
);
},
createProject(payload: { name: string; product: string; metadata?: Record<string, unknown> }) {
return request<Project>("/api/projects/", { method: "POST", body: JSON.stringify(payload) });
},
+31 -3
View File
@@ -523,6 +523,10 @@ export function PipelinePage(props: {
// 行38/流程步骤4 · 单卡生成 loading:按「busyKey」分别记录,各按钮互不影响(生成三视图不挡新增人物)
const [genBusy, setGenBusy] = useState<Set<string>>(new Set());
// 刷新后从后端「认领」在途出图任务:出图在 worker 跑、内存态 genBusy 丢了,据此重建占位卡 loading
const [pendingGen, setPendingGen] = useState<Array<{ kind: string; label: string; is_triview: boolean }>>([]);
const pendingHas = (kind: string, label: string) =>
pendingGen.some((p) => p.kind === kind && !p.is_triview && (p.label || "") === (label || ""));
const isBusy = (k: string) => genBusy.has(k);
const addBusy = (k: string) => setGenBusy((s) => { const n = new Set(s); n.add(k); return n; });
const delBusy = (k: string) => setGenBusy((s) => { const n = new Set(s); n.delete(k); return n; });
@@ -1849,6 +1853,30 @@ export function PipelinePage(props: {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeDot, viewStage, project.id]);
// 流程步骤4 · 在基础资产趴轮询后端在途出图任务,重建「生成中」占位卡 loading(刷新后内存态丢失也能恢复);
// 在途数减少=有任务出图完成 → 刷新项目把占位卡换成真卡。离开该趴即停。
const prevPendingCountRef = useRef(0);
useEffect(() => {
if (activeDot !== 2 && viewStage !== 2) return;
let stopped = false;
let timer = 0;
const tick = async () => {
try {
const res = await api.pendingAssets(project.id);
if (stopped) return;
const list = res.pending || [];
if (list.length < prevPendingCountRef.current) void onRefreshProject();
prevPendingCountRef.current = list.length;
setPendingGen(list.map((p) => ({ kind: p.kind, label: p.label, is_triview: p.is_triview })));
} catch {
/* 忽略,下一轮再试 */
}
if (!stopped) timer = window.setTimeout(tick, 4000);
};
void tick();
return () => { stopped = true; window.clearTimeout(timer); };
}, [activeDot, viewStage, project.id, onRefreshProject]);
function goStage(n: number) {
setViewStage(n);
setNavigated(true);
@@ -2214,7 +2242,7 @@ export function PipelinePage(props: {
// 行39 · 商品三视图:一个商品组,candidate_assets = 各版本,adopted_asset = 采用版
const productGroup = groupsByKind("product").find((g) => !isTriview(g)) || null;
const productVersions = productGroup?.candidate_assets ?? [];
const triGenerating = isBusy("tri:product");
const triGenerating = isBusy("tri:product") || pendingHas("product", "");
const adoptedTriAsset = productGroup?.adopted_asset || "";
const previewTriAsset = (triPreviewId && productVersions.includes(triPreviewId)) ? triPreviewId : adoptedTriAsset;
const hasTriView = productVersions.length > 0;
@@ -2354,7 +2382,7 @@ export function PipelinePage(props: {
{pendingTags.map((tag) => {
const seedKey = `seed:${kind}:${tag}`;
const promptValue = assetPromptDraft[seedKey] ?? tagPrompt(tag);
const busy = isBusy(seedKey) || isBusy(`${seedKey}:tri`);
const busy = isBusy(seedKey) || isBusy(`${seedKey}:tri`) || pendingHas(kind, tag);
return (
<div className="asset-card-2 asset-seed" data-asset-kind={kind} data-seed-tag={tag} key={seedKey}>
<div className="placeholder thumb-2">
@@ -2379,7 +2407,7 @@ export function PipelinePage(props: {
const mainUrl = groupMainUrl(grp);
const promptValue = assetPromptDraft[entity.key] ?? grp.prompt ?? "";
const entBK = `ent:${kind}:${entity.key}`;
const busy = isBusy(entBK) || isBusy(`${entBK}:tri`);
const busy = isBusy(entBK) || isBusy(`${entBK}:tri`) || pendingHas(kind, entity.name);
const rs = kind === "person" && grp.adopted_asset ? assetReview(grp.adopted_asset) : "";
return (
<div className="asset-card-2" data-asset-kind={kind} data-asset-id={entity.key} key={entity.key}>