diff --git a/src/components/overview/OverviewPage.tsx b/src/components/overview/OverviewPage.tsx index ecb2689..b684412 100644 --- a/src/components/overview/OverviewPage.tsx +++ b/src/components/overview/OverviewPage.tsx @@ -1,9 +1,203 @@ -export function OverviewPage() { +import { useState } from "react"; +import { useQuery, useQueries } from "@tanstack/react-query"; +import { listBuffedProjects, getBlueprint, getProjects, type BuffStatus, type BlueprintData, type Project } from "../../lib/commands"; +import { BlueprintModal } from "../dashboard/BlueprintModal"; + +// ── 时间格式化 ──────────────────────────────────────────────────────────────── + +function relativeTime(iso: string): string { + const diff = Date.now() - new Date(iso).getTime(); + const m = Math.floor(diff / 60000); + if (m < 1) return "刚刚"; + if (m < 60) return `${m} 分钟前`; + const h = Math.floor(m / 60); + if (h < 24) return `${h} 小时前`; + return `${Math.floor(h / 24)} 天前`; +} + +// ── 单个项目卡片 ────────────────────────────────────────────────────────────── + +function BuffProjectCard({ + buff, + blueprint, + projectName, + onOpenBlueprint, +}: { + buff: BuffStatus; + blueprint?: BlueprintData | null; + projectName: string; + onOpenBlueprint: () => void; +}) { + const tasks = blueprint?.manifest.modules.flatMap((m) => m.tasks) ?? []; + const total = tasks.length; + const done = tasks.filter((t) => t.status === "done").length; + const inProgress = tasks.filter((t) => t.status === "in_progress").length; + const todo = tasks.filter((t) => t.status === "todo").length; + const blocked = tasks.filter((t) => t.status === "blocked").length; + const progress = total > 0 ? Math.round((done / total) * 100) : 0; + return ( -
-
📊
-

仪表盘

-

待规划:提醒、动态、快捷入口等

+
+
+
+ {projectName} + ⚡ Buff +
+ +
+ + {total > 0 && ( +
+
+ {done}/{total} 完成 + {progress}% +
+
+
+
+
+ )} + + {total > 0 && ( +
+ {inProgress > 0 && 🔵 {inProgress} 进行中} + {todo > 0 && 📋 {todo} 待做} + {blocked > 0 && 🔴 {blocked} 受阻} + {inProgress === 0 && todo === 0 && blocked === 0 && ( + ✅ 全部完成 + )} +
+ )} + +
+ {buff.project_path} + {buff.last_sync_at ? ( + 同步 {relativeTime(buff.last_sync_at)} + ) : ( + 尚未同步 + )} +
+
+ ); +} + +// ── 主页面 ─────────────────────────────────────────────────────────────────── + +export function OverviewPage() { + const [openModal, setOpenModal] = useState<{ projectName: string; projectPath: string; projectId: string } | null>(null); + + const { data: buffs = [], isLoading: buffsLoading } = useQuery({ + queryKey: ["buffed-projects"], + queryFn: listBuffedProjects, + staleTime: 10000, + }); + + const { data: projects = [] } = useQuery({ + queryKey: ["projects"], + queryFn: () => getProjects(), + staleTime: 60000, + }); + + const blueprintResults = useQueries({ + queries: buffs.map((b) => ({ + queryKey: ["blueprint", b.project_path], + queryFn: () => getBlueprint(b.project_path), + staleTime: 30000, + })), + }); + + const projectMap = new Map((projects as Project[]).map((p: Project) => [p.id, p])); + + const getName = (buff: BuffStatus) => + (projectMap.get(buff.project_id) as Project | undefined)?.name + ?? buff.project_path.split(/[/\\]/).filter(Boolean).pop() + ?? buff.project_path; + + // 跨项目进行中任务 + const inProgressTasks = buffs.flatMap((b, i) => { + const bp = blueprintResults[i]?.data as BlueprintData | null | undefined; + if (!bp) return []; + return bp.manifest.modules + .flatMap((m) => m.tasks) + .filter((t) => t.status === "in_progress") + .map((t) => ({ title: t.title, projectName: getName(b) })); + }); + + if (buffsLoading) { + return ( +
加载中…
+ ); + } + + if (buffs.length === 0) { + return ( +
+
+

暂无激活的蓝图 Buff

+

在项目蓝图 → 治理面板中为项目施加 Buff

+
+ ); + } + + return ( +
+
+
+

仪表盘

+

{buffs.length} 个项目已施加蓝图 Buff

+
+ + {/* 跨项目进行中任务 */} + {inProgressTasks.length > 0 && ( +
+

🔵 跨项目进行中

+
+ {inProgressTasks.map((item, i) => ( +
+ {item.title} + {item.projectName} +
+ ))} +
+
+ )} + + {/* Buff 项目卡片 */} +
+

⚡ Buff 项目

+
+ {buffs.map((buff, i) => ( + + setOpenModal({ + projectName: getName(buff), + projectPath: buff.project_path, + projectId: buff.project_id, + }) + } + /> + ))} +
+
+
+ + {openModal && ( + setOpenModal(null)} + /> + )}
); }