feat(*): 工作台界面 - 优化插件中心、管理侧栏及画布与短剧列表

This commit is contained in:
ddCat
2026-08-22 11:35:23 +08:00
parent 9327b75c2e
commit c670ebca90
13 changed files with 792 additions and 179 deletions

View File

@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState, type ReactNode } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import { useInfiniteQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { App, Button, Form, Input, Modal, Select } from "antd";
import { ArrowRight, BookOpenText, FileText, FolderKanban, Images, LayoutGrid, Palette, Plus, Search, Sparkles, Trash2 } from "lucide-react";
import { Link, useNavigate, useSearchParams } from "react-router";
@@ -112,7 +112,14 @@ export default function ProjectsPage() {
setGenerationPreview("");
}
};
const query = useQuery({ queryKey: ["projects"], queryFn: listProjects });
const loadMoreRef = useRef<HTMLDivElement>(null);
const query = useInfiniteQuery({
// 分页查询和画布页的全量项目查询不能共用缓存形状,否则两个页面会互相覆盖缓存数据。
queryKey: ["projects", "paged"],
queryFn: ({ pageParam }) => listProjects({ page: pageParam, pageSize: 50 }),
initialPageParam: 1,
getNextPageParam: (lastPage) => (lastPage.hasMore ? lastPage.page + 1 : undefined),
});
const mutation = useMutation({
mutationFn: createProject,
onSuccess: ({ project }) => {
@@ -140,9 +147,10 @@ export default function ProjectsPage() {
onOk: () => deleteMutation.mutate(projectId),
});
};
const allProjects = useMemo(() => query.data?.pages.flatMap((page) => page.projects) || [], [query.data]);
const rows = useMemo(() => {
const normalizedKeyword = keyword.trim().toLowerCase();
return [...(query.data?.projects || [])]
return [...allProjects]
.filter(({ project }) => status === "all" || project.status === status)
.filter(({ project }) => !normalizedKeyword || `${project.name} ${project.description} ${project.stylePresetId} ${parseStyleProfile(project.styleProfileJson)?.title || resolveCanvasStylePreset(project.stylePresetId)?.title || ""}`.toLowerCase().includes(normalizedKeyword))
.sort((left, right) => {
@@ -150,17 +158,23 @@ export default function ProjectsPage() {
if (sort === "progress") return projectSummaryCompletion(right) - projectSummaryCompletion(left);
return right.project.updatedAt.localeCompare(left.project.updatedAt);
});
}, [keyword, query.data, sort, status]);
}, [allProjects, keyword, sort, status]);
const totalProjectCount = query.data?.pages[0]?.total ?? allProjects.length;
useEffect(() => {
const node = loadMoreRef.current;
if (!node || !query.hasNextPage || query.isError) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry?.isIntersecting && !query.isFetchingNextPage) void query.fetchNextPage();
},
{ rootMargin: "600px" },
);
observer.observe(node);
return () => observer.disconnect();
}, [query.fetchNextPage, query.hasNextPage, query.isError, query.isFetchingNextPage]);
const hasInitialError = query.isError && !query.data;
return (
<WorkspacePage className="library-page" grid>
<div className="studio-band">
<PageHeader
title="短剧创作"
description="从故事出发,推进章节、画布与镜头。"
meta={<span className="app-projects-header-meta">{rows.length} 个</span>}
actions={null}
/>
</div>
<section className="app-story-create-panel mt-4" aria-label="开始一部新短剧">
<div className="app-story-create-head">
<div className="app-story-create-title">
@@ -217,14 +231,17 @@ export default function ProjectsPage() {
<Select className="w-32" value={sort} onChange={setSort} options={[{ label: "最近更新", value: "updated" }, { label: "章节进度", value: "progress" }, { label: "项目名称", value: "name" }]} />
</ListToolbar>
{query.isError ? <WorkspaceErrorState description={query.error instanceof Error ? query.error.message : "项目列表加载失败"} onRetry={() => void query.refetch()} /> : null}
{hasInitialError ? <WorkspaceErrorState description={query.error instanceof Error ? query.error.message : "项目列表加载失败"} onRetry={() => void query.refetch()} /> : null}
{query.isLoading ? <WorkspaceLoadingState label="正在整理项目" detail="读取章节、画布与资产进度" /> : null}
{!query.isLoading && !query.isError && rows.length ? (
{!query.isLoading && !hasInitialError && rows.length ? (
<CollectionGrid className="library-grid project-library-grid">
{rows.map((row) => <ProjectRow key={row.project.id} row={row} onDelete={() => confirmDeleteProject(row.project.id, row.project.name)} />)}
</CollectionGrid>
) : null}
{!query.isLoading && !rows.length && !query.isError && (keyword || status !== "all") ? (
{!query.isLoading && !hasInitialError ? <div ref={loadMoreRef} className="library-load-more" aria-live="polite">
{query.isFetchingNextPage ? "正在加载更多项目…" : query.isError ? <button type="button" onClick={() => void query.fetchNextPage()}>加载更多失败,点击重试</button> : query.hasNextPage ? "继续下滑加载更多(每页 50 条)" : allProjects.length ? `已加载全部 ${totalProjectCount} 个项目` : null}
</div> : null}
{!query.isLoading && !rows.length && !hasInitialError && (keyword || status !== "all") ? (
<WorkspaceState
icon="projects"
title={keyword || status !== "all" ? "没有匹配的项目" : "创建第一个故事项目"}