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:
@@ -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) });
|
||||
},
|
||||
|
||||
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user