feat: 任务中心批次弹窗加「加入资产库」状态+单图/一键切换 (row38)

任务中心点已完成批次的弹窗里,每张生成图现在:
- 显示「已入库」绿标 + 入库态橙描边
- 右下角单图「加入资产库 / 取消」按钮(单图粒度,复用 Asset.in_library 真门控)
- 弹窗头部「一键全部加入资产库 / 全部移出」批量切换
乐观更新 batchImgs+assets,失败回滚;生成时已扣费,此口只控是否进资产库列表、不二次扣费。
弹窗 createPortal 到 body 不在 .image-workbench 作用域,故 .modal 作用域补一份 gen 网格/徽标/按钮样式。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
zyc
2026-06-30 11:02:28 +08:00
co-authored by Claude Opus 4.8
parent 291a8bf394
commit 38881c3041
2 changed files with 100 additions and 1 deletions
+44
View File
@@ -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;
+56 -1
View File
@@ -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<string>, 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
<div className="ic-m"><LayoutGrid size={17} /></div>
<div className="ti">{openBatch.label}<span>// {openBatch.count} 张 · {(openBatch.created_at || "").slice(0, 10)}</span></div>
<span style={{ flex: 1 }} />
{/* row38:一键全部加入资产库 / 全部移出(整批切换) */}
{batchWithId.length > 0 && (
<button className={`btn btn-sm${allInLib ? "" : " btn-primary"}`} type="button" disabled={libBusy} style={{ marginRight: 8 }} onClick={toggleAllBatchLibrary}>
<Bookmark size={13} />
{allInLib ? "全部移出资产库" : "一键全部加入资产库"}
</button>
)}
<button className="x" type="button" aria-label="关闭" onClick={() => setOpenBatch(null)}><X size={16} /></button>
</div>
<div className="modal-b">
@@ -464,13 +502,30 @@ export function AssetFactoryPage({ navigate }: { navigate: (page: Page) => void
<div className="gen-images" style={{ "--cols": Math.min(4, batchImgs.length), "--ratio": "1 / 1" } as React.CSSProperties}>
{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 (
<div className="gen-image" key={a.id}>
<div className={`gen-image${inLib && u ? " adopted" : ""}`} key={a.id}>
{u ? (
<img className="gen-image-img" src={u} alt={a.name} loading="lazy" style={{ cursor: "zoom-in" }} onClick={() => setPreview({ src: u, name: a.name })} />
) : (
<div className="placeholder"><span className="ph-frame">#{i + 1}</span></div>
)}
{/* row38:左上「已入库」绿标 + 右下角加入/取消按钮(单图粒度) */}
{u && inLib && (
<span className="gen-adopt-badge"><Check size={11} /></span>
)}
{u && (
<button
type="button"
className={`tc-lib-btn${inLib ? " on" : ""}`}
title={inLib ? "取消加入资产库" : "加入资产库"}
disabled={libBusy}
onClick={() => toggleBatchImgLibrary(a)}
>
<Bookmark size={13} />
{inLib ? "取消" : "加入资产库"}
</button>
)}
</div>
);
})}