import { useEffect, useState } from "react"; import type { BillingSummary, Product, Project } from "../types"; import type { NavigateFn } from "./route-config"; import { api } from "../api"; import { SkeletonRows } from "../components/loading"; import { money, stageMeta, statusPill } from "./stage-config"; import { IconKitSvg } from "../components/IconKitSvg"; import { ProductCreateDrawer, type ProductCreatePayload } from "../components/product-create-drawer"; // 工作台「最近项目」进度条:4 段(与视频项目菜单一致) // script→1 / base_assets→2 / storyboard→3 / video|export→4 // completed:全 done(绿);failed:当前格 fail(红),之前 done;其余:之前 done,当前 cur(橙闪) const DASH_STAGE_TOTAL = 4; const DASH_STAGE_NO: Record = { script: 1, base_assets: 2, storyboard: 3, video: 4, export: 4 }; 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 ? "cur" : ""; } function DashProgress({ project }: { project: Project }) { const no = project.status === "completed" ? DASH_STAGE_TOTAL : (DASH_STAGE_NO[project.current_stage] || 1); return (
{Array.from({ length: DASH_STAGE_TOTAL }, (_, k) => k + 1).map((i) => ( ))}
); } export function Dashboard({ products, projects, productTotal, projectTotal, billing, userName, loading = false, navigate, onCreateProduct }: { products: Product[]; projects: Project[]; productTotal?: number; // 后端真实总数(分页 count),用于「N 个/SKU」展示 projectTotal?: number; billing: BillingSummary | null; userName?: string; loading?: boolean; navigate: NavigateFn; // YYX#11:工作台点「新建商品」直接弹抽屉(复用商品库同一组件),而非跳商品库 onCreateProduct?: (payload: ProductCreatePayload) => Promise | void; }) { // YYX#11:新建商品抽屉开关 const [createDrawer, setCreateDrawer] = useState(false); const projCount = projectTotal ?? projects.length; const prodCount = productTotal ?? products.length; // 资产总数:走轻量 summary 接口(各 tab 计数之和),不再吃全局 assets 全量数组 const [assetCount, setAssetCount] = useState(null); useEffect(() => { let alive = true; api.assetSummary() .then((c) => { if (alive) setAssetCount(Object.values(c).reduce((a, b) => a + b, 0)); }) .catch(() => {}); return () => { alive = false; }; }, []); const completed = projects.filter((project) => project.status === "completed").length; const running = projects.filter((project) => !["completed", "failed"].includes(project.status)).length; const productTitle = (id: string) => products.find((product) => product.id === id)?.title || "商品"; // 日期 mono:`// 06.19 · 周四`(对齐 V1 mono 注释风,补零到两位) const now = new Date(); const pad2 = (n: number) => String(n).padStart(2, "0"); const weekdays = ["日", "一", "二", "三", "四", "五", "六"]; const dateMono = `${pad2(now.getMonth() + 1)}.${pad2(now.getDate())} · 周${weekdays[now.getDay()]}`; // 「总项目」delta = 本月新建数(真实,按 created_at 算)。 // 但 projects 仅首页分页数据;当存在未加载的项目(projCount > 已加载)时,本月数会少算 → 宁可不显示也不显假值。 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 allProjectsLoaded = projects.length >= projCount; const totalDelta = allProjectsLoaded && newThisMonth > 0 ? `↑ 本月 +${newThisMonth}` : undefined; // 余额 mini progress:真实冻结占比 = 冻结 /(可用 + 冻结);无 budget/已用字段,故不复刻 V1「已用 ¥X/¥Y」,改显真实「冻结 ¥X / 共 ¥Y」。 const balanceNum = Number(billing?.account.balance ?? 0); const reservedNum = Number(billing?.account.reserved_balance ?? 0); const fundsTotal = balanceNum + reservedNum; const reservedPct = fundsTotal > 0 ? Math.min(100, Math.round((reservedNum / fundsTotal) * 100)) : 0; return ( <>

欢迎回来{userName ? `,${userName.split("@")[0]}` : ""}

// {dateMono}·你有 {running} 个项目 正在进行中
++ navigate("projects", { tab: "all" })} /> navigate("projects", { tab: "wip" })} /> navigate("projects", { tab: "done" })} />

最近项目

{loading && projects.length === 0 ? : projects.slice(0, 6).map((project) => )}

快捷入口

[ /shortcuts ]
navigate("products")} /> navigate("library")} /> navigate("account")} /> navigate("projects")} />

提示

[ FAQ ]
扣费规则生成失败、超时、用户重跑 — 均不扣费。仅在你点 [ 确认通过 ] 时按 token 实际结算。
{/* YYX#11:新建商品抽屉 · 复用商品库同一组件,创建成功后跳商品库看新商品 */} {onCreateProduct && ( setCreateDrawer(false)} onCreate={onCreateProduct} onCreated={() => { setCreateDrawer(false); navigate("products"); }} /> )} ); } export function KpiStat({ label, badge, value, delta, onClick }: { label: string; badge: string; value: string | number; delta?: string; onClick?: () => void }) { const inner = <>
{label} {badge}
{value}
{delta ?
{delta}
: null}; // 可点的统计块复用现有 .stat 类(与余额块一致),不新造样式/色值。 if (onClick) return ; return
{inner}
; } export function Shortcut({ name, title, desc, onClick }: { name: string; title: string; desc: string; onClick: () => void }) { return
{title}
{desc}
; }