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

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

No files matched your search

+14 -11
View File
@@ -164,16 +164,17 @@ export default function AssetsPage() {
const start = (page - 1) * pageSize;
return filteredAssets.slice(start, start + pageSize);
}, [filteredAssets, page, pageSize]);
const visibleAssets = useMemo(
() => (assetPageQuery.data?.assets || localVisibleAssets).filter((asset): asset is LibraryAsset => asset.kind !== "entity"),
[assetPageQuery.data?.assets, localVisibleAssets],
);
// 远端分页结果可能只含被本页排除的 entity 角色卡;此时若直接采用远端结果,
// 本地已保存、尚未同步到服务端的素材会被遮蔽成空页,因此回退到本地筛选结果。
const remotePageAssets = useMemo(() => (assetPageQuery.data?.assets || []).filter((asset): asset is LibraryAsset => asset.kind !== "entity"), [assetPageQuery.data?.assets]);
const useRemotePage = remotePageAssets.length > 0;
const visibleAssets = useMemo(() => useRemotePage ? remotePageAssets : localVisibleAssets, [useRemotePage, remotePageAssets, localVisibleAssets]);
const visibleAssetIds = useMemo(() => visibleAssets.map((asset) => asset.id), [visibleAssets]);
const allFilteredSelected = visibleAssetIds.length > 0 && visibleAssetIds.every((id) => selectedIds.includes(id));
const canCreateAsset = page === 1 && viewMode === "library" && !keyword.trim() && kindFilter === "all" && categoryFilter === "all" && folderFilter === "all";
const totalAssets = assetPageQuery.data?.total ?? filteredAssets.length;
const kindCounts = useMemo(() => assetCountMap(kindOptions, assetPageQuery.data?.kindCounts, viewMode === "trash" ? trashAssets : activeAssets, (asset) => asset.kind), [activeAssets, assetPageQuery.data?.kindCounts, trashAssets, viewMode]);
const categoryCounts = useMemo(() => assetCountMap(categoryOptions, assetPageQuery.data?.categoryCounts, viewMode === "trash" ? trashAssets : activeAssets, (asset) => asset.category || "other"), [activeAssets, assetPageQuery.data?.categoryCounts, trashAssets, viewMode]);
const totalAssets = useRemotePage ? assetPageQuery.data?.total ?? filteredAssets.length : filteredAssets.length;
const kindCounts = useMemo(() => assetCountMap(kindOptions, useRemotePage ? assetPageQuery.data?.kindCounts : undefined, viewMode === "trash" ? trashAssets : activeAssets, (asset) => asset.kind), [activeAssets, assetPageQuery.data?.kindCounts, trashAssets, useRemotePage, viewMode]);
const categoryCounts = useMemo(() => assetCountMap(categoryOptions, useRemotePage ? assetPageQuery.data?.categoryCounts : undefined, viewMode === "trash" ? trashAssets : activeAssets, (asset) => asset.category || "other"), [activeAssets, assetPageQuery.data?.categoryCounts, trashAssets, useRemotePage, viewMode]);
const folderCounts = assetPageQuery.data?.folderCounts || {};
useEffect(() => {
@@ -525,7 +526,7 @@ export default function AssetsPage() {
<PageHeader
title={viewMode === "trash" ? "素材库 / 回收站" : "素材库"}
description={viewMode === "trash" ? "已删除画布或手动归档的临时素材,可随时还原或彻底清理。" : "管理文本、图片、视频、音频和 3D 模型素材。"}
meta={<span className="app-projects-header-meta assets-header-meta">{validAssets.length} 个素材</span>}
meta={<span className="app-projects-header-meta assets-header-meta">{totalAssets} 个素材</span>}
actions={
<div className="assets-header-actions">
<div className="assets-header-action-buttons">
@@ -725,7 +726,7 @@ export default function AssetsPage() {
onDelete={() => setBatchDeleteOpen(true)}
/>
) : null}
{validAssets.length === 0 ? (
{validAssets.length === 0 && totalAssets === 0 ? (
viewMode === "trash" ? (
<WorkspaceState icon="assets" compact title="回收站是空的" description="删除画布或手动移入回收站的素材会暂存到这里,可在需要时随时还原。" />
) : (
@@ -733,7 +734,7 @@ export default function AssetsPage() {
)
) : (
<>
{filteredAssets.length === 0 ? (
{visibleAssets.length === 0 ? (
<WorkspaceState icon="assets" compact title="没有匹配的素材" description="调整关键词或左侧分类后再试。" />
) : (
<CollectionGrid className="library-grid assets-library-grid" style={{ "--assets-grid-columns": gridDensity } as React.CSSProperties}>
@@ -1519,7 +1520,9 @@ function readAssetGridDensity(): AssetGridDensity {
function assetCountMap<T extends { label: string; value: string }>(options: T[], remote: Record<string, number> | undefined, fallback: LibraryAsset[], valueOf: (asset: LibraryAsset) => string) {
const result = new Map<string, number>();
options.forEach((option) => {
if (remote) result.set(option.value, option.value === "all" ? Object.values(remote).reduce((sum, count) => sum + count, 0) : remote[option.value] || 0);
// 列表只展示 LibraryAsset(entity 角色卡被排除);"全部"计数只能累加选项里声明的类型,
// 否则远端 facets 里的 entity 会计入"全部",出现计数 30 但列表为空的矛盾。
if (remote) result.set(option.value, option.value === "all" ? options.reduce((sum, item) => item.value === "all" ? sum : sum + (remote[item.value] || 0), 0) : remote[option.value] || 0);
else result.set(option.value, option.value === "all" ? fallback.length : fallback.filter((asset) => valueOf(asset) === option.value).length);
});
return result;