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:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user