import { useMemo, useState } from "react"; import { ArrowRight, ChevronRight, FolderKanban, Replace, ScanSearch, Trash2, WandSparkles, } from "lucide-react"; import type { BillingSummary, Product, Project } from "../types"; import type { NavigateFn, Page } from "./route-config"; import { ConfirmModal } from "../components/overlays"; import { hasQuickCreateSuffix, isQuickCreateBusy } from "../quick-create-lock"; type DashTab = "all" | "wip" | "done"; type EntryTone = "primary" | "subtle"; const CREATE_GROUPS: Array<{ label: string; hint: string; cards: Array<{ title: string; desc: string; tone: EntryTone; page: Page; icon: "wand" | "folder" | "scan" | "replace"; }>; }> = [ { label: "从商品开始", hint: "围绕商品卖点生成完整带货内容", cards: [ { title: "一键成片", desc: "上传商品图片,自动完成整条视频", tone: "primary", page: "quickCreate", icon: "wand" }, { title: "专业创作", desc: "控制脚本、资产与视频生成", tone: "primary", page: "projectWizard", icon: "folder" }, ], }, { label: "从参考视频开始", hint: "复用成熟视频的镜头表达与节奏", cards: [ { title: "提炼提示词", desc: "上传参考视频,提炼可编辑提示词", tone: "subtle", page: "videoRemix", icon: "scan" }, { title: "视频复刻", desc: "保留参考视频的人物、场景与节奏,复刻出自己的带货内容", tone: "subtle", page: "videoReplace", icon: "replace" }, ], }, ]; const DASH_STAGE_TOTAL = 5; // 故事板已下线;storyboard 保留映射只为老项目的历史 current_stage 兜底 const DASH_STAGE_NO: Record = { script: 1, base_assets: 2, storyboard: 2, video: 3, export: 4 }; const DASH_TABS: Array<{ filter: DashTab; label: string }> = [ { filter: "all", label: "全部" }, { filter: "wip", label: "进行中" }, { filter: "done", label: "已成片" }, ]; function dashBucket(project: Project): DashTab { return project.status === "completed" ? "done" : "wip"; } function dashStageNo(project: Project) { return project.status === "completed" ? DASH_STAGE_TOTAL : (DASH_STAGE_NO[project.current_stage] || 1); } function dashProgClass(project: Project, i: number, no: number): string { if (project.status === "completed") return i <= no ? "done" : ""; if (project.status === "failed") return i === no ? "fail" : i < no ? "done" : ""; return i < no ? "done" : i === no ? "warn" : ""; } function dashStageLabel(project: Project): string { if (project.status === "failed") return "失败"; const map: Record = { script: "脚本", base_assets: "资产创建", storyboard: "资产创建", video: "视频生成", export: "视频生成", }; return map[project.current_stage] || "视频生成"; } function isQuickCreateProject(project: Project) { if (project.quick_create) return true; if (project.metadata?.quick_create) return true; return hasQuickCreateSuffix(project.name); } function dashCardMeta(project: Project, productTitle: string): string { const shots = project.video_segment_count ?? project.video_segments?.length ?? 0; const mode = isQuickCreateBusy(project) ? "一键成片生成中" : isQuickCreateProject(project) ? "一键成片" : "专业创作"; return [mode, productTitle, shots ? `${shots} 镜` : null].filter(Boolean).join(" / "); } function EntryIcon({ name }: { name: "wand" | "folder" | "scan" | "replace" }) { if (name === "folder") return ; if (name === "scan") return ; if (name === "replace") return ; return ; } export function Dashboard({ products, projects, productTotal: _productTotal, projectTotal, billing: _billing, userName, loading: _loading = false, navigate, onDelete, }: { products: Product[]; projects: Project[]; productTotal?: number; projectTotal?: number; billing: BillingSummary | null; userName?: string; loading?: boolean; navigate: NavigateFn; onDelete: (projectId: string) => Promise | void; }) { const [tab, setTab] = useState("all"); const [deleteTarget, setDeleteTarget] = useState(null); const projCount = projectTotal ?? projects.length; const completed = projects.filter((project) => project.status === "completed").length; const running = projects.filter((project) => !["completed", "failed"].includes(project.status)).length; const counts = { all: projCount, wip: running, done: completed, }; const now = new Date(); const pad2 = (n: number) => String(n).padStart(2, "0"); const dateLabel = `${pad2(now.getMonth() + 1)}.${pad2(now.getDate())}`; const monthStart = new Date(now.getFullYear(), now.getMonth(), 1).getTime(); const newThisMonth = projects.filter((p) => p.created_at && new Date(p.created_at).getTime() >= monthStart).length; const greetName = userName ? userName.split("@")[0] : ""; const productTitle = (id: string) => products.find((product) => product.id === id)?.title || "商品"; const listed = useMemo(() => { const rows = tab === "all" ? projects : projects.filter((project) => dashBucket(project) === tab); return rows.slice(0, 7); }, [projects, tab]); return (

欢迎回来{greetName ? `,${greetName}` : ""}

{dateLabel} · 你有{running}个项目正在进行中

开始创作
{CREATE_GROUPS.map((group) => (
{group.label} {group.hint}
{group.cards.map((card) => ( ))}
))}
项目概览

我的项目

{DASH_TABS.map((item) => ( ))}
{listed.length === 0 ? (
当前筛选下暂无展示项目
) : (
{listed.map((project) => { const no = dashStageNo(project); const cover = project.cover_preview_url || ""; const openProject = () => navigate("pipeline", { projectId: project.id }); return (
{ if (event.key === "Enter" || event.key === " ") { event.preventDefault(); openProject(); } }} role="button" tabIndex={0} >
{cover ? : 9:16}

{project.name}

{dashCardMeta(project, productTitle(project.product))}

当前阶段 {dashStageLabel(project)}
{Array.from({ length: DASH_STAGE_TOTAL }, (_, k) => k + 1).map((i) => ( ))}
); })}
)}
} detail={`确定删除「${deleteTarget?.name || ""}」?将移至「垃圾桶」,可在垃圾桶里恢复或彻底删除。`} confirmText="删除" onCancel={() => setDeleteTarget(null)} onConfirm={async () => { if (!deleteTarget) return; await onDelete(deleteTarget.id); setDeleteTarget(null); }} />
); }