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 (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center h-full text-center text-gray-300 select-none">
|
<div className="bg-white rounded-xl border border-gray-100 p-4 space-y-3 hover:border-purple-200 transition-colors">
|
||||||
<div className="text-5xl mb-4">📊</div>
|
<div className="flex items-center justify-between gap-2">
|
||||||
<p className="text-base font-medium text-gray-400">仪表盘</p>
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
<p className="text-xs text-gray-500 mt-1">待规划:提醒、动态、快捷入口等</p>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user