diff --git a/core/frontend/src/ai-tools-page.css b/core/frontend/src/ai-tools-page.css index 4977ee8..0991e2d 100644 --- a/core/frontend/src/ai-tools-page.css +++ b/core/frontend/src/ai-tools-page.css @@ -955,6 +955,50 @@ } .image-workbench .gen-adopt-badge svg { width: 11px; height: 11px; } +/* row38:任务中心批次弹窗(createPortal 到 body,不在 .image-workbench 作用域内)—— + 自带一份 gen 网格 + 入库徽标/单图加入按钮样式,复用主工作台视觉。 */ +.modal .gen-images { + display: grid; + grid-template-columns: repeat(var(--cols, 4), 1fr); + gap: 10px; +} +.modal .gen-image { + position: relative; + aspect-ratio: var(--ratio, 1 / 1); + border-radius: var(--r-md); + overflow: hidden; + background: + repeating-linear-gradient(135deg, rgba(0, 0, 0, 0.025) 0 1px, transparent 1px 12px), + var(--black-alpha-4); +} +.modal .gen-image-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; } +.modal .gen-image .placeholder { position: absolute; inset: 0; display: grid; place-items: center; } +.modal .gen-image.adopted { outline: 2px solid var(--accent-forest); outline-offset: -2px; } +.modal .gen-adopt-badge { + position: absolute; top: 8px; left: 8px; + display: inline-flex; align-items: center; gap: 4px; + height: 22px; padding: 0 8px 0 6px; + background: var(--accent-forest); color: var(--accent-white); + font-family: var(--font-mono); font-size: 11px; font-weight: 600; letter-spacing: .02em; + border-radius: var(--r-pill); z-index: 3; +} +.modal .gen-adopt-badge svg { width: 11px; height: 11px; } +/* 单图「加入资产库 / 取消」按钮:右下角悬浮,hover 出现;已入库态常驻橙 */ +.tc-lib-btn { + position: absolute; right: 8px; bottom: 8px; z-index: 3; + display: inline-flex; align-items: center; gap: 4px; + height: 26px; padding: 0 10px; + border: 0; border-radius: var(--r-pill); + background: rgba(38, 38, 38, .82); color: var(--accent-white); + font-size: 11px; font-weight: 500; letter-spacing: .02em; + cursor: pointer; opacity: 0; + transition: opacity var(--t-base), background var(--t-base); +} +.modal .gen-image:hover .tc-lib-btn { opacity: 1; } +.tc-lib-btn.on { background: var(--heat); opacity: 1; } +.tc-lib-btn:disabled { opacity: .5; cursor: not-allowed; } +.tc-lib-btn svg { width: 13px; height: 13px; } + /* 底部操作按钮行(§4.18 .gen-card-actions · 二级 + ghost · 不放主橙) */ .image-workbench .gen-card-actions { display: flex; gap: 8px; diff --git a/core/frontend/src/routes/ai-tools.tsx b/core/frontend/src/routes/ai-tools.tsx index 00ed0e1..a8bc498 100644 --- a/core/frontend/src/routes/ai-tools.tsx +++ b/core/frontend/src/routes/ai-tools.tsx @@ -214,6 +214,37 @@ export function AssetFactoryPage({ navigate }: { navigate: (page: Page) => void return () => document.removeEventListener("click", close); }, [openChip]); + // row38:任务中心批次弹窗 —— 每张生成图都可「加入资产库 / 取消加入」,真源是后端 Asset.in_library。 + // 乐观更新 batchImgs(弹窗)+ assets(列表 cover 用),失败回滚。资产生成时已落库扣费,此口只控是否进资产库列表、不二次扣费。 + const [libBusy, setLibBusy] = useState(false); + function patchInLib(ids: Set, next: boolean) { + setBatchImgs((prev) => prev.map((a) => (ids.has(a.id) ? { ...a, in_library: next } : a))); + setAssets((prev) => prev.map((a) => (ids.has(a.id) ? { ...a, in_library: next } : a))); + } + async function toggleBatchImgLibrary(asset: Asset) { + if (!asset.id || libBusy) return; + const next = !asset.in_library; + const ids = new Set([asset.id]); + patchInLib(ids, next); + try { await api.setAssetLibrary(asset.id, next); } + catch { patchInLib(ids, !next); } + } + // 一键:有未入库 → 全部加入;已全部入库 → 全部移出 + const batchWithId = batchImgs.filter((a) => a.id); + const allInLib = batchWithId.length > 0 && batchWithId.every((a) => a.in_library); + async function toggleAllBatchLibrary() { + if (!batchWithId.length || libBusy) return; + const next = !allInLib; + const targets = batchWithId.filter((a) => a.in_library !== next).map((a) => a.id); + if (!targets.length) return; + const ids = new Set(targets); + setLibBusy(true); + patchInLib(ids, next); + try { await Promise.all(targets.map((id) => api.setAssetLibrary(id, next))); } + catch { patchInLib(ids, !next); } + finally { setLibBusy(false); } + } + const typeOptions = Array.from(new Set(taskBatches.map((b) => b.label).filter(Boolean))); const TIME_OPTS: Array<{ value: typeof timeFilter; label: string }> = [ { value: "all", label: "全部时间" }, { value: "1", label: "今天" }, { value: "7", label: "近 7 天" }, { value: "30", label: "近 30 天" } @@ -453,6 +484,13 @@ export function AssetFactoryPage({ navigate }: { navigate: (page: Page) => void
{openBatch.label}// {openBatch.count} 张 · {(openBatch.created_at || "").slice(0, 10)}
+ {/* row38:一键全部加入资产库 / 全部移出(整批切换) */} + {batchWithId.length > 0 && ( + + )}
@@ -464,13 +502,30 @@ export function AssetFactoryPage({ navigate }: { navigate: (page: Page) => void
{batchImgs.map((a, i) => { const u = a.files?.find((f) => f.preview_url)?.preview_url || a.files?.[0]?.preview_url || ""; + const inLib = !!a.in_library; return ( -
+
{u ? ( {a.name} setPreview({ src: u, name: a.name })} /> ) : (
#{i + 1}
)} + {/* row38:左上「已入库」绿标 + 右下角加入/取消按钮(单图粒度) */} + {u && inLib && ( + 已入库 + )} + {u && ( + + )}
); })}