perf(*): 创作工作台 - 优化首页按需加载与画布交互并修复历史迁移冲突
This commit is contained in:
@@ -2,6 +2,8 @@ import { App, Button, Dropdown, Modal, Popconfirm } from "antd";
|
||||
import type { MenuProps } from "antd";
|
||||
import { Check, ChevronDown, FileText, FolderOpen, HardDrive, Image as ImageIcon, LoaderCircle, Music2, Puzzle, RotateCcw, Search, Trash2, Upload, UserRound, Video } from "lucide-react";
|
||||
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useUserStore } from "@/stores/use-user-store";
|
||||
|
||||
import { AssetMediaPreview } from "@/components/asset-media-preview";
|
||||
import { AssetLibraryCard } from "@/components/assets/asset-library-card";
|
||||
@@ -10,7 +12,7 @@ import { PaginationBar } from "@/components/layout/workspace-page";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ExternalAssetPickerReference } from "@/lib/plugins/plugin-types";
|
||||
import { flushAssetStorePersistence, useAssetStore, type Asset } from "@/stores/use-asset-store";
|
||||
import { deleteAssetWithRemoteSync, saveRemoteUserDataNow } from "@/services/user-data-sync";
|
||||
import { deleteAssetWithRemoteSync, loadAssetLibraryPage, saveRemoteUserDataNow } from "@/services/user-data-sync";
|
||||
|
||||
export type AssetLibraryPickerItem = {
|
||||
id: string;
|
||||
@@ -36,6 +38,8 @@ export type AssetLibraryPickerFolder = {
|
||||
};
|
||||
|
||||
type Props = {
|
||||
remoteLibrary?: boolean;
|
||||
remoteKind?: string;
|
||||
open: boolean;
|
||||
items: AssetLibraryPickerItem[];
|
||||
categoryLabels: Record<string, string>;
|
||||
@@ -70,6 +74,8 @@ type Props = {
|
||||
};
|
||||
|
||||
export function AssetLibraryPickerModal({
|
||||
remoteLibrary = false,
|
||||
remoteKind,
|
||||
open,
|
||||
items,
|
||||
categoryLabels,
|
||||
@@ -104,6 +110,28 @@ export function AssetLibraryPickerModal({
|
||||
const [working, setWorking] = useState(false);
|
||||
const [uploadingCount, setUploadingCount] = useState(0);
|
||||
const [error, setError] = useState("");
|
||||
const userId = useUserStore((state) => state.user?.id);
|
||||
const sessionHydrated = useUserStore((state) => state.hydrated);
|
||||
const [remotePage, setRemotePage] = useState(1);
|
||||
const [remotePageSize, setRemotePageSize] = useState(40);
|
||||
const [remoteKeyword, setRemoteKeyword] = useState("");
|
||||
const remoteEnabled = remoteLibrary && Boolean(userId) && source === "local";
|
||||
useEffect(() => {
|
||||
const timer = window.setTimeout(() => setRemoteKeyword(keyword.trim()), 250);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [keyword]);
|
||||
useEffect(() => setRemotePage(1), [category, remoteKeyword, open]);
|
||||
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 }),
|
||||
enabled: remoteEnabled && open && sessionHydrated,
|
||||
});
|
||||
const remoteItems = useMemo<AssetLibraryPickerItem[]>(() => (remoteQuery.data?.assets || []).filter((asset) => asset.kind !== "entity" && asset.kind !== "model").map((asset) => ({
|
||||
id: asset.id, title: asset.title, category: asset.category || "other", archived: asset.status === "archived", asset,
|
||||
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 uploadInputRef = useRef<HTMLInputElement>(null);
|
||||
const initialSelectedIdsRef = useRef(initialSelectedIds);
|
||||
const itemsRef = useRef(items);
|
||||
@@ -116,12 +144,12 @@ 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 : localItems;
|
||||
const sourceItems = source === "plugin" ? pluginItems : remoteEnabled ? remoteItems : localItems;
|
||||
const activeSourceItems = useMemo(() => sourceItems.filter((item) => !item.archived), [sourceItems]);
|
||||
const archivedItems = useMemo(() => sourceItems.filter((item) => item.archived), [sourceItems]);
|
||||
const sourceFolders = source === "plugin" ? folders : [];
|
||||
const showCategories = source === "local" || !sourceFolders.length;
|
||||
const normalCategories = useMemo(() => ["all", ...Array.from(new Set(activeSourceItems.map((item) => item.category || "other"))).filter((value) => value !== "all")], [activeSourceItems]);
|
||||
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 archivedCount = archivedItems.length;
|
||||
const isRecycleBin = category === "archived";
|
||||
|
||||
@@ -134,9 +162,9 @@ export function AssetLibraryPickerModal({
|
||||
return false;
|
||||
}
|
||||
if (folderId !== "all" && (item.folderId || "") !== folderId) return false;
|
||||
return !query || [item.title, item.searchText || "", item.description || ""].join(" ").toLowerCase().includes(query);
|
||||
return remoteEnabled || !query || [item.title, item.searchText || "", item.description || ""].join(" ").toLowerCase().includes(query);
|
||||
});
|
||||
}, [category, folderId, keyword, sourceItems]);
|
||||
}, [category, folderId, keyword, sourceItems, remoteEnabled]);
|
||||
const selectedIds = useMemo(
|
||||
() =>
|
||||
Array.from(selected).filter((id) => {
|
||||
@@ -221,6 +249,7 @@ export function AssetLibraryPickerModal({
|
||||
message.warning("已在本地还原,稍后自动同步至云端");
|
||||
} finally {
|
||||
setWorking(false);
|
||||
if (remoteEnabled) void remoteQuery.refetch();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -235,6 +264,7 @@ export function AssetLibraryPickerModal({
|
||||
message.error(err instanceof Error ? err.message : "删除失败");
|
||||
} finally {
|
||||
setWorking(false);
|
||||
if (remoteEnabled) void remoteQuery.refetch();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -245,12 +275,13 @@ export function AssetLibraryPickerModal({
|
||||
try {
|
||||
for (const item of toDelete) await deleteAssetWithRemoteSync(item.id);
|
||||
setSelected(new Set());
|
||||
message.success(`已清空回收站 ${toDelete.length} 个素材`);
|
||||
message.success(`已删除${remoteEnabled ? "当前页" : "回收站"} ${toDelete.length} 个素材`);
|
||||
setCategory("all");
|
||||
} catch (err) {
|
||||
message.error(err instanceof Error ? err.message : "清空回收站失败");
|
||||
} finally {
|
||||
setWorking(false);
|
||||
if (remoteEnabled) void remoteQuery.refetch();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -367,7 +398,7 @@ export function AssetLibraryPickerModal({
|
||||
<input value={keyword} onChange={(event) => setKeyword(event.target.value)} placeholder="搜索素材名称或标签" aria-label="搜索素材" />
|
||||
</label>
|
||||
<span className="asset-picker-count">
|
||||
已选 {selectedIds.length} · {pagination ? pagination.total : visibleItems.length} 个素材
|
||||
已选 {selectedIds.length} · {effectivePagination ? effectivePagination.total : visibleItems.length} 个素材
|
||||
</span>
|
||||
</header>
|
||||
<div className="asset-picker-body">
|
||||
@@ -391,7 +422,7 @@ export function AssetLibraryPickerModal({
|
||||
<span className="assets-filter-count">{countFor(value)}</span>
|
||||
</button>
|
||||
))}
|
||||
{archivedCount > 0 ? (
|
||||
{archivedCount > 0 || remoteEnabled ? (
|
||||
<div className="mt-3 border-t border-border/40 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
@@ -412,7 +443,7 @@ export function AssetLibraryPickerModal({
|
||||
</nav>
|
||||
<div className="asset-picker-grid-wrap">
|
||||
<div className="asset-picker-grid">
|
||||
{loading ? (
|
||||
{remoteEnabled && remoteQuery.isError ? <div role="alert">素材读取失败<Button onClick={() => void remoteQuery.refetch()}>重试</Button></div> : loading || (remoteEnabled && remoteQuery.isFetching) ? (
|
||||
<div className="asset-picker-empty">
|
||||
<LoaderCircle className="animate-spin" />
|
||||
<strong>正在读取素材</strong>
|
||||
@@ -428,7 +459,7 @@ export function AssetLibraryPickerModal({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{pagination ? <PaginationBar alwaysShow current={pagination.current} pageSize={pagination.pageSize} total={pagination.total} itemLabel="项" pageSizeOptions={[20, 40, 80]} onChange={pagination.onChange} /> : null}
|
||||
{effectivePagination ? <PaginationBar alwaysShow current={effectivePagination.current} pageSize={effectivePagination.pageSize} total={effectivePagination.total} itemLabel="项" pageSizeOptions={[20, 40, 80]} onChange={effectivePagination.onChange} /> : null}
|
||||
</div>
|
||||
</div>
|
||||
<footer className={cn("asset-picker-footer", !activeUpload && "is-compact")}>
|
||||
@@ -456,9 +487,9 @@ export function AssetLibraryPickerModal({
|
||||
<div className="asset-picker-actions">
|
||||
{isRecycleBin ? (
|
||||
<>
|
||||
<Popconfirm title="确认清空回收站?" description="清空后所有回收站素材及其物理文件将被彻底删除,不可恢复。" onConfirm={handleEmptyRecycleBin} okText="清空" okButtonProps={{ danger: true }} cancelText="取消">
|
||||
<Popconfirm title={remoteEnabled ? "确认删除当前页回收站素材?" : "确认清空回收站?"} description="仅删除当前列表中的素材;仍被引用的素材由服务端拒绝删除。删除不可恢复。" onConfirm={handleEmptyRecycleBin} okText="删除" okButtonProps={{ danger: true }} cancelText="取消">
|
||||
<Button type="text" danger disabled={working || !archivedCount}>
|
||||
清空回收站
|
||||
{remoteEnabled ? "删除当前页" : "清空回收站"}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
<Popconfirm title="确认彻底删除已选素材?" onConfirm={handleDeleteSelected} okText="删除" okButtonProps={{ danger: true }} cancelText="取消">
|
||||
|
||||
Reference in New Issue
Block a user