fix(core): 资产详情大图随数据显示,缩略图已出大图不再卡转圈

- 三视图/立绘大图改为跟数据(url)走,出图完成即显示,不再被在途轮询的
  busy 标志挡住——根治「小缩略图已出、大图一直转圈」
- 一并带上工作区其余改动(script_agent / services / tests / ai-tools / actor-library / App)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
zyc
2026-06-18 10:57:02 +08:00
co-authored by Claude Opus 4.8
parent fe38d3a512
commit 326c34ecd1
7 changed files with 200 additions and 47 deletions
+24 -7
View File
@@ -18,7 +18,7 @@ export function ActorLibrary({ open, mode, initialStudio, assets, onClose, onPic
initialStudio?: boolean; // 打开即进「添加人物工作台」
assets: Asset[];
onClose: () => void;
onPick?: (assetId: string) => void | Promise<unknown>;
onPick?: (assetId: string, assetName?: string) => void | Promise<unknown>;
onGenerate: (prompt: string) => Promise<{ assets: Asset[] } | null>;
onUpload: (file: File) => Promise<Asset | null>;
// 据选中立绘生成配套三视图(后端吃任意 person 资产 id,不依赖该资产已在某 base group)
@@ -71,6 +71,14 @@ export function ActorLibrary({ open, mode, initialStudio, assets, onClose, onPic
const people = useMemo(() => source.filter((a) => a.category === "person" && previewOf(a)), [source]);
const list = people.filter((a) => (tab === "preset" ? isPreset(a) : !isPreset(a)));
// 演员库分页(客户端):一页 10 个(5 列 × 2 行)。切 tab / 列表变化时回第 1 页,避免停在越界页。
const PAGE_SIZE = 10;
const [page, setPage] = useState(1);
const pageCount = Math.max(1, Math.ceil(list.length / PAGE_SIZE));
useEffect(() => { setPage(1); }, [tab, open]);
useEffect(() => { setPage((p) => Math.min(p, pageCount)); }, [pageCount]);
const pageList = list.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
// 选某张候选立绘 → 进右侧栏(预填名字,清三视图标志)
function pickCandidate(a: Asset) {
setPicked(a);
@@ -126,8 +134,8 @@ export function ActorLibrary({ open, mode, initialStudio, assets, onClose, onPic
<div className="actorlib-bg" onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}>
<div className="actorlib" role="dialog" aria-modal="true" aria-label="演员库">
<div className="actorlib-h">
<h2>{mode === "replace" ? "演员库 · 选择演员替换" : "演员库"}</h2>
<span className="mono" style={{ fontSize: 12, color: "var(--black-alpha-48)" }}>// {mode === "replace" ? "点演员卡即替换当前立绘" : "平台预设演员 / 我的演员"}</span>
<h2>{mode === "replace" ? "演员库 · 选择演员" : "演员库"}</h2>
<span className="mono" style={{ fontSize: 12, color: "var(--black-alpha-48)" }}>// {mode === "replace" ? "点演员卡即选用应用" : "平台预设演员 / 我的演员"}</span>
<button className="x" type="button" aria-label="关闭" onClick={onClose}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M6 6l12 12M6 18L18 6" /></svg>
</button>
@@ -230,15 +238,16 @@ export function ActorLibrary({ open, mode, initialStudio, assets, onClose, onPic
</button>
</div>
{list.length ? (
<>
<div className="actorlib-grid">
{list.map((a) => {
{pageList.map((a) => {
const url = previewOf(a);
return (
<div className="actor-card" key={a.id}>
<div className={`placeholder actor-thumb${url ? " has-mock-media" : ""}`} style={url ? mediaStyle(url) : undefined}
role="button" tabIndex={0} title={mode === "replace" ? "选用此演员替换" : "查看大图"}
onClick={() => { if (mode === "replace") { void onPick?.(a.id); } else if (url) { setPreview({ src: url, name: a.name }); } }}
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); if (mode === "replace") { void onPick?.(a.id); } else if (url) { setPreview({ src: url, name: a.name }); } } }}>
role="button" tabIndex={0} title={mode === "replace" ? "选用此演员" : "查看大图"}
onClick={() => { if (mode === "replace") { void onPick?.(a.id, a.name); } else if (url) { setPreview({ src: url, name: a.name }); } }}
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); if (mode === "replace") { void onPick?.(a.id, a.name); } else if (url) { setPreview({ src: url, name: a.name }); } } }}>
{!url && <span className="ph-frame">{a.name}</span>}
{mode === "replace" && <span className="actor-pick"></span>}
</div>
@@ -247,6 +256,14 @@ export function ActorLibrary({ open, mode, initialStudio, assets, onClose, onPic
);
})}
</div>
{pageCount > 1 && (
<div className="actorlib-pager" style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 12, marginTop: 16 }}>
<button className="btn btn-ghost btn-sm" type="button" disabled={page <= 1} onClick={() => setPage((p) => Math.max(1, p - 1))}></button>
<span className="mono" style={{ fontSize: 12, color: "var(--black-alpha-48)" }}>{page} / {pageCount}</span>
<button className="btn btn-ghost btn-sm" type="button" disabled={page >= pageCount} onClick={() => setPage((p) => Math.min(pageCount, p + 1))}></button>
</div>
)}
</>
) : (
<div className="placeholder" style={{ minHeight: 160, flexDirection: "column", gap: 10 }}>
<span className="ph-frame">// {tab === "preset" ? "暂无平台预设演员" : "还没有自己的演员 · 点右上「添加演员」"}</span>