From 669d9a683b3c6c4204e5690f48717530e74ae84e Mon Sep 17 00:00:00 2001 From: zyc <1439655764@qq.com> Date: Thu, 25 Jun 2026 14:08:58 +0800 Subject: [PATCH] =?UTF-8?q?polish:=20=E5=95=86=E5=93=81/=E5=9C=BA=E6=99=AF?= =?UTF-8?q?=E4=B8=89=E8=A7=86=E5=9B=BE=E3=80=8C=E6=9B=BF=E6=8D=A2=E3=80=8D?= =?UTF-8?q?=E9=80=89=E5=9B=BE=E5=B1=82=E2=80=94=E2=80=94=E9=80=9A=E7=94=A8?= =?UTF-8?q?=E5=8C=96=20+=20=E5=9C=BA=E6=99=AF=E4=BF=AE=E5=A4=8D=20+=20?= =?UTF-8?q?=E8=BF=87=E6=BB=A4=E5=88=86=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 商品三视图「替换」数据源修复:旧实现只读 productRecord.images(封面1张) → 改为 fetch 后端 ?product= 三路并集,只列商品图(product_image)。 - 页脚样式对齐人物/场景卡:hstack 左 ghost「替换」+ 右 primary「AI 生成」。 - 场景卡「替换」修复:旧实现误用人物专用演员库(列出角色数据)→ 场景改走 通用选图层,按 category=scene 取;人物仍走演员库。覆盖 seed/已生成/详情 3 入口。 - 场景过滤:故事板帧也被存成 category=scene(metadata 全空,只能按名字), 过滤掉 storyboard/分镜,只留真场景图。 - 商品/场景选图均加分页,20 个一页(上一页 / X·共N / 下一页)。 Co-Authored-By: Claude Opus 4.8 --- core/frontend/src/routes/pipeline.tsx | 147 ++++++++++++++++++-------- 1 file changed, 105 insertions(+), 42 deletions(-) diff --git a/core/frontend/src/routes/pipeline.tsx b/core/frontend/src/routes/pipeline.tsx index 46f4b61..b51f9dc 100644 --- a/core/frontend/src/routes/pipeline.tsx +++ b/core/frontend/src/routes/pipeline.tsx @@ -603,34 +603,74 @@ export function PipelinePage(props: { // 每次生成=一个 product group=一版三视图;预览态(triPreviewId)只切主图,采用态存 metadata.product_tri_group const [triPanelOpen, setTriPanelOpen] = useState(false); const [triPreviewId, setTriPreviewId] = useState(null); - // 问题5(a):用「已有素材」直接当最终三视图——选商品已有图 / 上传本地图,attach 成商品组采用版(不走平台 AI 生成、不计费)。 - const [triPickOpen, setTriPickOpen] = useState(false); - const [triPickBusy, setTriPickBusy] = useState(false); - const triFileRef = useRef(null); - async function attachExistingTri(assetId: string) { - setTriPickBusy(true); + // 通用「选已有素材替换」选图层。商品三视图(取 product_image、按商品过滤)与场景(取 scene、团队全量)共用一套。 + // 人物不走这里——人物有专门的演员库(ActorLibrary,含模特库/三视图工作台),对人物是对的。 + // 旧 bug:场景卡「替换」误用了演员库 → 列的是角色数据。现在场景改走本选图层。 + type AssetPick = { + title: string; + category: "product_image" | "scene"; + product?: string; // 传则按商品过滤(商品三视图);不传则团队全量(场景) + target: { group_id?: string; kind?: "product" | "person" | "scene"; label?: string }; + uploadCategory: string; + uploadName: string; + }; + const [assetPick, setAssetPick] = useState(null); + const [assetPickBusy, setAssetPickBusy] = useState(false); + const [assetPickList, setAssetPickList] = useState([]); + const [assetPickLoading, setAssetPickLoading] = useState(false); + const [assetPickPage, setAssetPickPage] = useState(1); + const ASSET_PICK_PAGE_SIZE = 20; // 商品/场景选图:20 个一页 + const pickFileRef = useRef(null); + const pickPreview = (a: Asset): string => a.files?.find((f) => f.is_primary)?.preview_url || a.files?.[0]?.preview_url || ""; + useBodyScrollLock(Boolean(assetPick)); + useEffect(() => { + if (!assetPick) return; + setAssetPickLoading(true); + setAssetPickList([]); + setAssetPickPage(1); + // 后端 ?product= 取三路并集(独立生图 metadata.product_id + 项目内生成 origin_task→project→product + 上传图); + // 不传 product 则团队全量(场景常跨商品复用)。category 限定只列该类素材,排除噪音。多取一点供前端分页。 + api.assetsPage({ ...(assetPick.product ? { product: assetPick.product } : {}), asset_type: "image", category: assetPick.category, pageSize: 200, ordering: "-created_at" }) + .then((res) => setAssetPickList(res.results || [])) + .catch(() => setAssetPickList([])) + .finally(() => setAssetPickLoading(false)); + }, [assetPick]); + // 场景里混进了故事板帧(也被存成 category=scene,只能靠名字区分;metadata 全空)→ 过滤掉,只留真场景图。 + const assetPickFiltered = useMemo(() => { + if (!assetPick) return []; + return assetPick.category === "scene" + ? assetPickList.filter((a) => !/storyboard|story-board|分镜/i.test(a.name || "")) + : assetPickList; + }, [assetPick, assetPickList]); + const assetPickPageCount = Math.max(1, Math.ceil(assetPickFiltered.length / ASSET_PICK_PAGE_SIZE)); + useEffect(() => { setAssetPickPage((p) => Math.min(p, assetPickPageCount)); }, [assetPickPageCount]); + const assetPickPageList = assetPickFiltered.slice((assetPickPage - 1) * ASSET_PICK_PAGE_SIZE, assetPickPage * ASSET_PICK_PAGE_SIZE); + async function attachPickedAsset(assetId: string) { + if (!assetPick) return; + setAssetPickBusy(true); try { - await onAttachBaseAsset({ kind: "product" }, assetId); // 后端 attach-base-asset 按 kind=product 命中/建商品组并采用 - setTriPickOpen(false); + await onAttachBaseAsset(assetPick.target, assetId); // 后端 attach-base-asset 按 group_id / kind+label 命中或建组并采用 + setAssetPick(null); } finally { - setTriPickBusy(false); + setAssetPickBusy(false); } } - async function uploadTriFromFile(file: File) { - setTriPickBusy(true); + async function uploadPickedFromFile(file: File) { + if (!assetPick) return; + setAssetPickBusy(true); try { const fd = new FormData(); fd.append("file", file); fd.append("asset_type", "image"); - fd.append("category", "tri_view"); // 标三视图类(与生成的三视图同类,送审/展示口径一致) - fd.append("name", `${productName || "商品"}·三视图`); + fd.append("category", assetPick.uploadCategory); + fd.append("name", assetPick.uploadName); const asset = await api.uploadAsset(fd); // 落 TOS + 建 team 资产,返回带 id - await onAttachBaseAsset({ kind: "product" }, asset.id); - setTriPickOpen(false); + await onAttachBaseAsset(assetPick.target, asset.id); + setAssetPick(null); } catch (e) { onNotify?.("error", e instanceof Error && e.message ? e.message : "上传失败,请重试"); } finally { - setTriPickBusy(false); + setAssetPickBusy(false); } } function jumpAssetSection(kind: "product" | "person" | "scene") { @@ -870,6 +910,13 @@ export function PipelinePage(props: { function openActorReplaceSeed(kind: "person" | "scene", tag: string) { setActorLib({ mode: "replace", seedKind: kind, seedLabel: tag }); } + // 场景「替换」:走通用选图层(取团队场景图),而非人物专用的演员库(修复:场景替换误列角色数据)。 + function openSceneReplaceSeed(tag: string) { + setAssetPick({ title: "选择场景素材", category: "scene", target: { kind: "scene", label: tag }, uploadCategory: "scene", uploadName: `${tag || "场景"}·场景图` }); + } + function openSceneReplaceEntity(groupId: string, label?: string) { + setAssetPick({ title: "选择场景素材", category: "scene", target: { group_id: groupId }, uploadCategory: "scene", uploadName: `${label || "场景"}·场景图` }); + } async function pickActor(assetId: string, assetName?: string) { if (actorLib?.mode === "replace") { if (actorLib.groupId) await onAttachBaseAsset({ group_id: actorLib.groupId }, assetId); @@ -2843,13 +2890,13 @@ export function PipelinePage(props: {
{(project.created_at || "").slice(0, 10)} 创建
- {/* 行39 · 点击展开右侧三视图预览面板并生成一版(对齐原版 prod-preview 交互) */} - - {/* 问题5(a):不想用平台生成 → 直接用已有素材(选商品图 / 上传本地图)当三视图 */} - + {/* 与人物/场景卡页脚一致:hstack 左「使用已有素材」(ghost) + spacer + 右「AI 生成」(primary) */} +
+ {/* 问题5(a):不想用平台生成 → 直接用已有素材(选商品图 / 上传本地图)当三视图 */} + + + +
{/* 行39 · 三视图预览面板(对齐原版 prod-preview):生成中转圈 → 主图(可放大)+ 重跑/采用 + 历史版本切换 */} @@ -2969,7 +3016,7 @@ export function PipelinePage(props: {
openSeedDetail(kind, tag, promptValue)}>{tag}来自脚本
setAssetPromptDraft((m) => ({ ...m, [seedKey]: v }))} />
- +
@@ -3031,7 +3078,7 @@ export function PipelinePage(props: {
- +
@@ -3948,7 +3995,10 @@ export function PipelinePage(props: { {busyPortrait ? : } {busyPortrait ? "生成中…" : (portraitVersions.length === 0 ? (isPerson ? "生成立绘" : "生成场景图") : (isPerson ? "重跑立绘" : "重跑"))} - + @@ -3969,32 +4019,45 @@ export function PipelinePage(props: { ); })()} {/* 问题5(a):用已有素材作三视图——选商品已有图 / 上传本地图,attach 成商品采用版(不走平台 AI 生成、不计费) */} - {triPickOpen && ( -
{ if (e.target === e.currentTarget && !triPickBusy) setTriPickOpen(false); }}> + {assetPick && ( +
{ if (e.target === e.currentTarget && !assetPickBusy) setAssetPick(null); }}>
-

使用已有素材作三视图

- // 选商品图或上传本地图,直接作最终三视图 -
- { const f = e.target.files?.[0]; if (f) void uploadTriFromFile(f); e.target.value = ""; }} /> + { const f = e.target.files?.[0]; if (f) void uploadPickedFromFile(f); e.target.value = ""; }} />
{/* 上传本地图 tile */} -
{ if (!triPickBusy) triFileRef.current?.click(); }} onKeyDown={(e) => { if ((e.key === "Enter" || e.key === " ") && !triPickBusy) { e.preventDefault(); triFileRef.current?.click(); } }}> - {triPickBusy ? "处理中…" : "+ 上传本地图"} +
{ if (!assetPickBusy) pickFileRef.current?.click(); }} onKeyDown={(e) => { if ((e.key === "Enter" || e.key === " ") && !assetPickBusy) { e.preventDefault(); pickFileRef.current?.click(); } }}> + {assetPickBusy ? "处理中…" : "+ 上传本地图"}
- {/* 商品已有图:点击直接采用为三视图 */} - {(productRecord?.images ?? []).map((img) => ( -
{ if (!triPickBusy) void attachExistingTri(img.asset); }} onKeyDown={(e) => { if ((e.key === "Enter" || e.key === " ") && !triPickBusy) { e.preventDefault(); void attachExistingTri(img.asset); } }}> - {!img.preview_url && // 无预览} -
- ))} + {/* 已有素材:当前页(点击直接采用) */} + {assetPickPageList.map((a) => { + const u = pickPreview(a); + return ( +
{ if (!assetPickBusy) void attachPickedAsset(a.id); }} onKeyDown={(e) => { if ((e.key === "Enter" || e.key === " ") && !assetPickBusy) { e.preventDefault(); void attachPickedAsset(a.id); } }}> + {!u && // 无预览} +
+ ); + })}
- {(productRecord?.images ?? []).length === 0 && ( -
// 该商品暂无已有图,可直接上传本地图
+ {assetPickLoading + ?
// 加载素材…
+ : assetPickFiltered.length === 0 && ( +
// 暂无可选素材,可直接上传本地图
+ )} + {/* 分页:与模特素材库一致(上一页 / X·N 共 / 下一页) */} + {assetPickPageCount > 1 && ( +
+ + {assetPickPage} / {assetPickPageCount} · 共 {assetPickFiltered.length} + +
)}