diff --git a/core/frontend/src/routes/products.tsx b/core/frontend/src/routes/products.tsx index 2bc3487..85b5094 100644 --- a/core/frontend/src/routes/products.tsx +++ b/core/frontend/src/routes/products.tsx @@ -677,33 +677,41 @@ export function ProductDetailPage({ product, projects, initialTab = "assets", na // ZWQ#row25/三视图同步:该商品已存在的「商品三视图」资产(在【视频项目】基础资产阶段生成、 // 或此前在商品库生成并落库)回填到三视图面板,避免「明明已生成却显示尚未生成」。 - // ⚠️ row28:必须只取「商品」三视图,排除「角色/人物三视图」——二者 category 都是 tri_view、名字都含「三视图」, - // 唯一可靠区分是 metadata.triview_of(角色三视图指向其立绘 asset id;商品三视图没有)+ kind=person。 - // 漏掉这层过滤就会把角色三视图塞进商品三视图面板(row28「商品三视图显示成角色三视图」)。 + // ⚠️ 关键:商品库生成的三视图 in_library=False(未加入资产库),普通 assetsPage 默认只返已入库的会漏掉它, + // 必须 in_library:"all" 专门拉一次(不影响 AI 素材 grid 仍只显已入库)。 + // ⚠️ row28:只取「商品」三视图,排除「角色/人物三视图」——二者 category 都是 tri_view、名字都含「三视图」, + // 唯一可靠区分是 metadata.triview_of(角色三视图指向其立绘;商品三视图没有)+ kind=person。 useEffect(() => { - const triAssets = productAssets.filter((a) => { - const m = (a.metadata as { triview_of?: unknown; kind?: string; view?: string } | undefined) || {}; - if (m.triview_of) return false; // 角色/人物/模特三视图:绝不进商品三视图面板 - if (m.kind === "person" || m.kind === "scene") return false; - return a.category === "tri_view" || a.asset_type === "tri_view" - || m.view === "three_view" || (a.name || "").includes("商品三视图"); - }); - if (!triAssets.length) return; - const serverVersions: TriVersion[] = triAssets - .map((a) => ({ id: a.id, url: a.files?.[0]?.preview_url || "", asset: a, createdAt: a.created_at || "" })) - .filter((v) => v.url); - if (!serverVersions.length) return; - setTriVersions((prev) => { - const seen = new Set(prev.map((v) => v.id)); - const merged = [...prev, ...serverVersions.filter((v) => !seen.has(v.id))]; - // 最新版本在前(与生成态 prepend 一致) - merged.sort((x, y) => (y.createdAt || "").localeCompare(x.createdAt || "")); - return merged.length === prev.length ? prev : merged; - }); - // 已被采用为商品封面的那版三视图 → 标记「已采用」 - const adopted = triAssets.find((a) => a.id === product.cover_asset); - if (adopted) setTriAdoptedId((cur) => cur || adopted.id); - }, [productAssets, product.cover_asset]); + if (!product?.id) return; + let alive = true; + api.assetsPage({ product: product.id, in_library: "all", asset_type: "image", pageSize: 200 }) + .then((res) => { + if (!alive) return; + const triAssets = (res.results || []).filter((a) => { + const m = (a.metadata as { triview_of?: unknown; kind?: string; view?: string } | undefined) || {}; + if (m.triview_of) return false; // 角色/人物/模特三视图:绝不进商品三视图面板 + if (m.kind === "person" || m.kind === "scene") return false; + return a.category === "tri_view" || a.asset_type === "tri_view" + || m.view === "three_view" || (a.name || "").includes("商品三视图"); + }); + const serverVersions: TriVersion[] = triAssets + .map((a) => ({ id: a.id, url: a.files?.[0]?.preview_url || "", asset: a, createdAt: a.created_at || "" })) + .filter((v) => v.url); + if (serverVersions.length) { + setTriVersions((prev) => { + const seen = new Set(prev.map((v) => v.id)); + const merged = [...prev, ...serverVersions.filter((v) => !seen.has(v.id))]; + merged.sort((x, y) => (y.createdAt || "").localeCompare(x.createdAt || "")); // 最新在前 + return merged.length === prev.length ? prev : merged; + }); + } + // 已被采用为商品封面的那版三视图 → 标记「已采用」 + const adopted = triAssets.find((a) => a.id === product.cover_asset); + if (adopted) setTriAdoptedId((cur) => cur || adopted.id); + }) + .catch(() => {}); + return () => { alive = false; }; + }, [product?.id, product.cover_asset, assetReload]); async function onPickProductImage(event: ChangeEvent) { const file = event.target.files?.[0];