feat(*): 工作台界面 - 优化插件中心、管理侧栏及画布与短剧列表
This commit is contained in:
@@ -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" ? "没有匹配的项目" : "创建第一个故事项目"}
|
||||
|
||||
Reference in New Issue
Block a user