diff --git a/web/src/components/assets/asset-library-picker-modal.tsx b/web/src/components/assets/asset-library-picker-modal.tsx index 13a324baa..acde4eff2 100644 --- a/web/src/components/assets/asset-library-picker-modal.tsx +++ b/web/src/components/assets/asset-library-picker-modal.tsx @@ -14,12 +14,20 @@ import type { ExternalAssetPickerReference } from "@/lib/plugins/plugin-types"; import { flushAssetStorePersistence, useAssetStore, type Asset } from "@/stores/use-asset-store"; import { deleteAssetWithRemoteSync, loadAssetLibraryPage, saveRemoteUserDataNow } from "@/services/user-data-sync"; +export type AssetPickerMediaKind = "image" | "video" | "audio" | "text"; + +export const ASSET_PICKER_MEDIA_KIND_LABELS: Record = { image: "图片", video: "视频", audio: "音频", text: "文本" }; + +const DEFAULT_MEDIA_KINDS: AssetPickerMediaKind[] = ["image", "video", "audio"]; + export type AssetLibraryPickerItem = { id: string; title: string; category: string; archived?: boolean; kindLabel: string; + /** 媒体类型筛选依据;缺省时按本地素材或插件素材的 kind 推断。 */ + mediaKind?: AssetPickerMediaKind; asset?: Asset; imageUrl?: string; imageStorageKey?: string; @@ -40,6 +48,8 @@ export type AssetLibraryPickerFolder = { type Props = { remoteLibrary?: boolean; remoteKind?: string; + /** 左侧「媒体类型」筛选项;只有一种类型或已由 remoteKind 固定时不展示该分组。 */ + mediaKinds?: AssetPickerMediaKind[]; open: boolean; items: AssetLibraryPickerItem[]; categoryLabels: Record; @@ -76,6 +86,7 @@ type Props = { export function AssetLibraryPickerModal({ remoteLibrary = false, remoteKind, + mediaKinds = DEFAULT_MEDIA_KINDS, open, items, categoryLabels, @@ -101,6 +112,7 @@ export function AssetLibraryPickerModal({ }: Props) { const { message } = App.useApp(); const [category, setCategory] = useState(initialCategory); + const [mediaKind, setMediaKind] = useState("all"); const [folderId, setFolderId] = useState(initialFolderId); const [source, setSource] = useState<"local" | "plugin">("local"); const [sourceMenuOpen, setSourceMenuOpen] = useState(false); @@ -120,10 +132,12 @@ export function AssetLibraryPickerModal({ const timer = window.setTimeout(() => setRemoteKeyword(keyword.trim()), 250); return () => window.clearTimeout(timer); }, [keyword]); - useEffect(() => setRemotePage(1), [category, remoteKeyword, open]); + useEffect(() => setRemotePage(1), [category, mediaKind, remoteKeyword, open]); + // remoteKind 是调用方写死的能力约束;媒体类型筛选只在没有该约束时参与服务端查询。 + const remoteQueryKind = remoteKind || (mediaKind === "all" ? undefined : mediaKind); const remoteQuery = useQuery({ - queryKey: ["asset-picker", userId, remotePage, remotePageSize, category, remoteKeyword, remoteKind], - queryFn: ({ signal }) => loadAssetLibraryPage({ page: remotePage, pageSize: remotePageSize, kind: remoteKind, category: category === "all" || category === "archived" || category === remoteKind ? undefined : category, status: category === "archived" ? "archived" : "active", query: remoteKeyword, signal }), + queryKey: ["asset-picker", userId, remotePage, remotePageSize, category, remoteKeyword, remoteQueryKind], + queryFn: ({ signal }) => loadAssetLibraryPage({ page: remotePage, pageSize: remotePageSize, kind: remoteQueryKind, category: category === "all" || category === "archived" || category === remoteQueryKind ? undefined : category, status: category === "archived" ? "archived" : "active", query: remoteKeyword, signal }), enabled: remoteEnabled && open && sessionHydrated, }); const remoteItems = useMemo(() => (remoteQuery.data?.assets || []).filter((asset) => asset.kind !== "entity" && asset.kind !== "model").map((asset) => ({ @@ -144,9 +158,15 @@ export function AssetLibraryPickerModal({ const localItems = useMemo(() => allItems.filter((item) => !item.external), [allItems]); const pluginItems = useMemo(() => allItems.filter((item) => Boolean(item.external)), [allItems]); const hasPluginSource = useMemo(() => Object.keys(categoryLabels).some((value) => value.startsWith("external:")) || pluginItems.some((item) => item.category.startsWith("external:")), [categoryLabels, pluginItems]); - const sourceItems = source === "plugin" ? pluginItems : remoteEnabled ? remoteItems : localItems; + // 媒体类型在分类之前收窄数据源,让左侧分类计数、网格和分页始终描述同一批素材。 + const sourceItems = useMemo(() => { + const base = source === "plugin" ? pluginItems : remoteEnabled ? remoteItems : localItems; + if (mediaKind === "all") return base; + return base.filter((item) => pickerItemMediaKind(item) === mediaKind); + }, [localItems, mediaKind, pluginItems, remoteEnabled, 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]); @@ -180,6 +200,7 @@ export function AssetLibraryPickerModal({ setFolderId(initialFolderId); setCategory(initialCategory); + setMediaKind("all"); setSource("local"); setKeyword(""); setUploadedItems([]); @@ -200,6 +221,11 @@ export function AssetLibraryPickerModal({ setSource("local"); }, [hasPluginSource, source]); + useEffect(() => { + if (mediaKind === "all" || mediaKindOptions.includes(mediaKind)) return; + setMediaKind("all"); + }, [mediaKind, mediaKindOptions]); + const selectSource = (nextSource: "local" | "plugin") => { if (nextSource === "plugin" && !hasPluginSource) return; setSource(nextSource); @@ -404,6 +430,16 @@ export function AssetLibraryPickerModal({