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
+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>
);
})}