import { useMemo, useState, type ReactNode } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { App, Button, Form, Input, Modal, Select } from "antd"; import { ArrowRight, BookOpenText, FolderKanban, Images, LayoutGrid, Plus, Search } from "lucide-react"; import { Link, useNavigate, useSearchParams } from "react-router"; import { ListToolbar, PageHeader, TableSurface, WorkspacePage } from "@/components/layout/workspace-page"; import { WorkspaceErrorState, WorkspaceLoadingState, WorkspaceState } from "@/components/layout/workspace-state"; import { projectSummaryCompletion, projectSummaryStage } from "@/lib/project-workbench"; import { createProject, listProjects, type ProjectSummary } from "@/services/api/projects"; import { sourceTypeLabel } from "./detail/shared"; type ProjectForm = { name: string; aspectRatio: string; sourceType: string }; export default function ProjectsPage() { const navigate = useNavigate(); const queryClient = useQueryClient(); const { message } = App.useApp(); const [searchParams, setSearchParams] = useSearchParams(); const [keyword, setKeyword] = useState(""); const [status, setStatus] = useState<"all" | "active" | "archived">("all"); const [sort, setSort] = useState<"updated" | "progress" | "name">("updated"); const createOpen = searchParams.get("create") === "1"; const setCreateOpen = (open: boolean) => { const next = new URLSearchParams(searchParams); if (open) next.set("create", "1"); else next.delete("create"); setSearchParams(next, { replace: true }); }; const query = useQuery({ queryKey: ["projects"], queryFn: listProjects }); const mutation = useMutation({ mutationFn: createProject, onSuccess: ({ project }) => { setCreateOpen(false); void queryClient.invalidateQueries({ queryKey: ["projects"] }); navigate(`/projects/${project.id}/overview`); }, onError: (error) => message.error(error instanceof Error ? error.message : "项目创建失败"), }); const rows = useMemo(() => { const normalizedKeyword = keyword.trim().toLowerCase(); return [...(query.data?.projects || [])] .filter(({ project }) => status === "all" || project.status === status) .filter(({ project }) => !normalizedKeyword || `${project.name} ${project.description} ${project.stylePresetId}`.toLowerCase().includes(normalizedKeyword)) .sort((left, right) => { if (sort === "name") return left.project.name.localeCompare(right.project.name, "zh-CN"); if (sort === "progress") return projectSummaryCompletion(right) - projectSummaryCompletion(left); return right.project.updatedAt.localeCompare(left.project.updatedAt); }); }, [keyword, query.data, sort, status]); return ( {rows.length} 个} actions={<>} /> { setKeyword(""); setStatus("all"); setSort("updated"); }}> } value={keyword} placeholder="搜索项目、简介或画风" onChange={(event) => setKeyword(event.target.value)} /> {query.isError ? void query.refetch()} /> : null} {query.isLoading ? : null} {!query.isLoading && rows.length ? (
项目当前阶段章节进度内容最近更新
{rows.map((row) => )}
) : null} {!query.isLoading && !rows.length && !query.isError ? ( } onClick={() => setCreateOpen(true)}>创建项目 : undefined} /> ) : null} setCreateOpen(false)} width={500} styles={{ body: { paddingTop: 12 } }}> layout="vertical" initialValues={{ aspectRatio: "9:16", sourceType: "blank" }} onFinish={(values) => mutation.mutate({ ...values, type: "short-drama" })}>

创建后先进入项目概览。章节、画风和参考资产可以逐步补充。

); } function ProjectRow({ row }: { row: ProjectSummary }) { const completion = projectSummaryCompletion(row); const stage = projectSummaryStage(row); return ( {row.project.name}{row.project.status === "archived" ? 已归档 : null}{row.project.stylePresetId || "未设置画风"} · {row.project.aspectRatio} · {sourceTypeLabel(row.project.sourceType)} 当前阶段 {stage.label}{stage.detail} 章节进度 {row.completedUnitCount}/{row.unitCount} 章{completion}% } label="章节" value={row.unitCount} /> } label="画布" value={row.canvasCount} /> } label="资产" value={row.assetCount} /> {formatProjectTime(row.project.updatedAt)} ); } function ProjectCount({ icon, label, value }: { icon: ReactNode; label: string; value: number }) { return {icon}{value}{label}; } function formatProjectTime(value: string) { return new Intl.DateTimeFormat("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }).format(new Date(value)); }