feat: 仪表盘 - 多项目蓝图聚合视图
This commit is contained in:
parent
00a6d4e335
commit
fbbfd4f205
@ -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 (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center text-gray-300 select-none">
|
||||
<div className="text-5xl mb-4">📊</div>
|
||||
<p className="text-base font-medium text-gray-400">仪表盘</p>
|
||||
<p className="text-xs text-gray-500 mt-1">待规划:提醒、动态、快捷入口等</p>
|
||||
<div className="bg-white rounded-xl border border-gray-100 p-4 space-y-3 hover:border-purple-200 transition-colors">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<span className="text-sm font-semibold text-gray-800 truncate">{projectName}</span>
|
||||
<span className="text-[10px] text-purple-600 bg-purple-50 border border-purple-200 px-1.5 py-0.5 rounded-full shrink-0">⚡ Buff</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onOpenBlueprint}
|
||||
className="text-[11px] text-blue-600 bg-blue-50 hover:bg-blue-100 px-2 py-1 rounded-lg shrink-0 transition-colors"
|
||||
>
|
||||
查看蓝图
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{total > 0 && (
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-[10px] text-gray-400">
|
||||
<span>{done}/{total} 完成</span>
|
||||
<span>{progress}%</span>
|
||||
</div>
|
||||
<div className="w-full h-1.5 bg-gray-100 rounded-full overflow-hidden">
|
||||
<div className="h-full rounded-full bg-green-400 transition-all" style={{ width: `${progress}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{total > 0 && (
|
||||
<div className="flex gap-3 flex-wrap">
|
||||
{inProgress > 0 && <span className="text-[11px] text-blue-600">🔵 {inProgress} 进行中</span>}
|
||||
{todo > 0 && <span className="text-[11px] text-amber-500">📋 {todo} 待做</span>}
|
||||
{blocked > 0 && <span className="text-[11px] text-red-500">🔴 {blocked} 受阻</span>}
|
||||
{inProgress === 0 && todo === 0 && blocked === 0 && (
|
||||
<span className="text-[11px] text-green-600">✅ 全部完成</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between text-[10px] text-gray-300 pt-1 border-t border-gray-50">
|
||||
<span className="truncate max-w-[60%]">{buff.project_path}</span>
|
||||
{buff.last_sync_at ? (
|
||||
<span>同步 {relativeTime(buff.last_sync_at)}</span>
|
||||
) : (
|
||||
<span>尚未同步</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 主页面 ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export function OverviewPage() {
|
||||
const [openModal, setOpenModal] = useState<{ projectName: string; projectPath: string; projectId: string } | null>(null);
|
||||
|
||||
const { data: buffs = [], isLoading: buffsLoading } = useQuery<BuffStatus[]>({
|
||||
queryKey: ["buffed-projects"],
|
||||
queryFn: listBuffedProjects,
|
||||
staleTime: 10000,
|
||||
});
|
||||
|
||||
const { data: projects = [] } = useQuery<Project[]>({
|
||||
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 (
|
||||
<div className="flex items-center justify-center h-full text-gray-300 text-sm">加载中…</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (buffs.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center text-gray-300 select-none">
|
||||
<div className="text-5xl mb-4">⚡</div>
|
||||
<p className="text-base font-medium text-gray-400">暂无激活的蓝图 Buff</p>
|
||||
<p className="text-xs text-gray-400 mt-1">在项目蓝图 → 治理面板中为项目施加 Buff</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-y-auto">
|
||||
<div className="max-w-2xl mx-auto px-6 py-6 space-y-6">
|
||||
<div>
|
||||
<h1 className="text-base font-semibold text-gray-800">仪表盘</h1>
|
||||
<p className="text-xs text-gray-400 mt-0.5">{buffs.length} 个项目已施加蓝图 Buff</p>
|
||||
</div>
|
||||
|
||||
{/* 跨项目进行中任务 */}
|
||||
{inProgressTasks.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-semibold text-gray-400 uppercase tracking-wider">🔵 跨项目进行中</p>
|
||||
<div className="space-y-1.5">
|
||||
{inProgressTasks.map((item, i) => (
|
||||
<div key={i} className="flex items-center justify-between bg-white rounded-lg border border-blue-100 px-3 py-2">
|
||||
<span className="text-xs text-gray-700 truncate flex-1">{item.title}</span>
|
||||
<span className="text-[10px] text-blue-400 shrink-0 ml-2">{item.projectName}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Buff 项目卡片 */}
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-semibold text-gray-400 uppercase tracking-wider">⚡ Buff 项目</p>
|
||||
<div className="space-y-3">
|
||||
{buffs.map((buff, i) => (
|
||||
<BuffProjectCard
|
||||
key={buff.project_path}
|
||||
buff={buff}
|
||||
blueprint={blueprintResults[i]?.data as BlueprintData | null | undefined}
|
||||
projectName={getName(buff)}
|
||||
onOpenBlueprint={() =>
|
||||
setOpenModal({
|
||||
projectName: getName(buff),
|
||||
projectPath: buff.project_path,
|
||||
projectId: buff.project_id,
|
||||
})
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{openModal && (
|
||||
<BlueprintModal
|
||||
projectName={openModal.projectName}
|
||||
projectPath={openModal.projectPath}
|
||||
projectId={openModal.projectId}
|
||||
onClose={() => setOpenModal(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user