fix(assets): 统一素材库 entity 口径并修复远端空结果遮蔽本地素材

- 个人素材库列表与 facets 统一排除 entity 类型,计数与列表同口径,修复“总数 30 但列表为空”
- 个人素材页 /assets、参考内容弹窗、项目资产页三处“远端空数组 truthy 遮蔽本地素材”统一修复:远端无可展示素材时回退本地列表,分页、分类、关键词过滤同步切换口径
- 项目资产页待确认候选不再硬编码 character 分类:分类切换联动查询,角色/道具/场景动态图标与文案,仅角色类展示“归并到角色”
- 项目资产页分类计数改为独立查询并与 tab 解耦,修复切 tab 时总数漂移;确认资产后同步失效候选与资产列表缓存;切分类重置候选页码
This commit is contained in:
李祥生
2026-09-15 17:23:24 +08:00
parent b7484c5678
commit 8db7cf0300
4 changed files with 68 additions and 36 deletions

View File

@@ -146,7 +146,10 @@ export function AssetLibraryPickerModal({
kindLabel: asset.kind === "image" ? "图片" : asset.kind === "video" ? "视频" : asset.kind === "audio" ? "音频" : "文本", searchText: (asset.tags ?? []).join(" "),
...(items.find((item) => item.id === asset.id) || { disabledReason: "此素材不适用于当前操作" }),
})), [remoteQuery.data, items]);
const effectivePagination = remoteEnabled ? { current: remotePage, pageSize: remotePageSize, total: remoteQuery.data?.total || 0, onChange: (page: number, pageSize: number) => { setRemotePage(page); setRemotePageSize(pageSize); } } : pagination;
// 远端分页可能没有任何可展示素材(本地素材尚未同步时远端为空);此时回退调用方传入的本地素材,
// 否则弹窗会被远端空结果遮蔽成空白。分页、分类与关键词过滤必须与数据源同步切换口径。
const useRemoteItems = remoteEnabled && remoteItems.length > 0;
const effectivePagination = useRemoteItems ? { current: remotePage, pageSize: remotePageSize, total: remoteQuery.data?.total || 0, onChange: (page: number, pageSize: number) => { setRemotePage(page); setRemotePageSize(pageSize); } } : pagination;
const uploadInputRef = useRef<HTMLInputElement>(null);
const initialSelectedIdsRef = useRef(initialSelectedIds);
const itemsRef = useRef(items);
@@ -161,16 +164,16 @@ export function AssetLibraryPickerModal({
const hasPluginSource = useMemo(() => Object.keys(categoryLabels).some((value) => value.startsWith("external:")) || pluginItems.some((item) => item.category.startsWith("external:")), [categoryLabels, pluginItems]);
// 媒体类型在分类之前收窄数据源,让左侧分类计数、网格和分页始终描述同一批素材。
const sourceItems = useMemo(() => {
const base = source === "plugin" ? pluginItems : remoteEnabled ? remoteItems : localItems;
const base = source === "plugin" ? pluginItems : useRemoteItems ? remoteItems : localItems;
if (mediaKind === "all") return base;
return base.filter((item) => pickerItemMediaKind(item) === mediaKind);
}, [localItems, mediaKind, pluginItems, remoteEnabled, remoteItems, source]);
}, [localItems, mediaKind, pluginItems, useRemoteItems, remoteItems, source]);
const activeSourceItems = useMemo(() => sourceItems.filter((item) => !item.archived), [sourceItems]);
const archivedItems = useMemo(() => sourceItems.filter((item) => item.archived), [sourceItems]);
const mediaKindOptions = useMemo(() => (remoteKind ? [] : Array.from(new Set(mediaKinds))), [mediaKinds, remoteKind]);
const sourceFolders = source === "plugin" ? folders : [];
const showCategories = source === "local" || !sourceFolders.length;
const normalCategories = useMemo(() => remoteEnabled ? Object.keys(categoryLabels).filter((value) => value !== "archived" && !value.startsWith("external:")) : ["all", ...Array.from(new Set(activeSourceItems.map((item) => item.category || "other"))).filter((value) => value !== "all")], [activeSourceItems, categoryLabels, remoteEnabled]);
const normalCategories = useMemo(() => useRemoteItems ? Object.keys(categoryLabels).filter((value) => value !== "archived" && !value.startsWith("external:")) : ["all", ...Array.from(new Set(activeSourceItems.map((item) => item.category || "other"))).filter((value) => value !== "all")], [activeSourceItems, categoryLabels, useRemoteItems]);
const archivedCount = archivedItems.length;
const isRecycleBin = category === "archived";
@@ -183,9 +186,9 @@ export function AssetLibraryPickerModal({
return false;
}
if (folderId !== "all" && (item.folderId || "") !== folderId) return false;
return remoteEnabled || !query || [item.title, item.searchText || "", item.description || ""].join(" ").toLowerCase().includes(query);
return useRemoteItems || !query || [item.title, item.searchText || "", item.description || ""].join(" ").toLowerCase().includes(query);
});
}, [category, folderId, keyword, sourceItems, remoteEnabled]);
}, [category, folderId, keyword, sourceItems, useRemoteItems]);
const selectedIds = useMemo(
() =>
Array.from(selected).filter((id) => {
@@ -481,7 +484,7 @@ export function AssetLibraryPickerModal({
</nav>
<div className="asset-picker-grid-wrap">
<div className="asset-picker-grid">
{remoteEnabled && remoteQuery.isError ? <div role="alert">素材读取失败<Button onClick={() => void remoteQuery.refetch()}>重试</Button></div> : loading || (remoteEnabled && remoteQuery.isFetching) ? (
{remoteEnabled && remoteQuery.isError ? <div role="alert">素材读取失败<Button onClick={() => void remoteQuery.refetch()}>重试</Button></div> : loading || (useRemoteItems && remoteQuery.isFetching) ? (
<div className="asset-picker-empty">
<LoaderCircle className="animate-spin" />
<strong>正在读取素材</strong>