239 lines
9.7 KiB
TypeScript
239 lines
9.7 KiB
TypeScript
import { Download, Settings, Sparkles, Users } from "lucide-react";
|
||
import { Pie, PieChart, ResponsiveContainer, Cell } from "recharts";
|
||
import type { SSTCOverview } from "@/api/sstc";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import { compactTitle } from "@/lib/title";
|
||
import { formatProjectNameWithId, toZhRole, toZhStatus } from "@/lib/display";
|
||
import { formatBeijingDateTime } from "@/lib/datetime";
|
||
|
||
interface SSTCGlobalHeaderProps {
|
||
overview: SSTCOverview;
|
||
onOpenAi: () => void;
|
||
onOpenSettings: () => void;
|
||
onExport: () => void;
|
||
onOpenWorkloads: () => void;
|
||
}
|
||
|
||
function statusVariant(status: string): "default" | "secondary" | "destructive" | "outline" {
|
||
const v = (status || "").toLowerCase();
|
||
if (v.includes("active") || v.includes("进行")) return "default";
|
||
if (v.includes("paused")) return "secondary";
|
||
if (v.includes("completed") || v.includes("done")) return "outline";
|
||
return "secondary";
|
||
}
|
||
|
||
function scoreColor(score: number): string {
|
||
if (score < 60) return "#dc2626";
|
||
if (score < 80) return "#ca8a04";
|
||
return "#16a34a";
|
||
}
|
||
|
||
function ratioColor(ratio: number): string {
|
||
if (ratio > 1.0) return "#dc2626";
|
||
if (ratio >= 0.7) return "#ca8a04";
|
||
return "#16a34a";
|
||
}
|
||
|
||
function ratioClass(ratio: number): string {
|
||
if (ratio > 1.0) return "text-rose-600";
|
||
if (ratio < 0.3) return "text-sky-600";
|
||
return "text-emerald-600";
|
||
}
|
||
|
||
function memberStatusLabel(status: "idle" | "normal" | "overloaded"): string {
|
||
if (status === "overloaded") return "过载";
|
||
if (status === "idle") return "空闲";
|
||
return "正常";
|
||
}
|
||
|
||
function memberStatusClass(status: "idle" | "normal" | "overloaded"): string {
|
||
if (status === "overloaded") return "bg-rose-100 text-rose-700";
|
||
if (status === "idle") return "bg-sky-100 text-sky-700";
|
||
return "bg-emerald-100 text-emerald-700";
|
||
}
|
||
|
||
function normalizeRoleLabel(rawRole: string): string {
|
||
const primaryRole = String(rawRole || "")
|
||
.split(",")
|
||
.map((item) => item.trim())
|
||
.find(Boolean);
|
||
return toZhRole(primaryRole || rawRole);
|
||
}
|
||
|
||
export function SSTCGlobalHeader({
|
||
overview,
|
||
onOpenAi,
|
||
onOpenSettings,
|
||
onExport,
|
||
onOpenWorkloads,
|
||
}: SSTCGlobalHeaderProps) {
|
||
const alignmentColor = scoreColor(overview.alignment_score || 0);
|
||
const progress = overview.task_progress.progress || 0;
|
||
const ratio = overview.workload_health.avg_ratio || 0;
|
||
const workloadMembers = overview.workload_health.members || [];
|
||
const projectLabel = formatProjectNameWithId({
|
||
id: overview.project.id,
|
||
title: overview.project.title,
|
||
});
|
||
const compactProjectTitle = compactTitle(projectLabel, 24);
|
||
|
||
return (
|
||
<div className="grid gap-4 xl:grid-cols-12">
|
||
<Card className="xl:col-span-3">
|
||
<CardHeader className="min-w-0 pb-3">
|
||
<CardTitle className="text-xl" title={projectLabel}>
|
||
{compactProjectTitle}
|
||
</CardTitle>
|
||
<div className="flex flex-wrap items-center gap-2">
|
||
<Badge variant={statusVariant(overview.project.status)}>
|
||
{toZhStatus(overview.project.status)}
|
||
</Badge>
|
||
<span className="text-xs text-muted-foreground">
|
||
链路编号: {overview.chain_id || "-"}
|
||
</span>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent className="space-y-1 text-sm text-muted-foreground">
|
||
<p>项目描述:{overview.project.description || "暂无"}</p>
|
||
<p>创建时间(北京时间):{formatBeijingDateTime(overview.project.created_at)}</p>
|
||
<p>任务总数:{overview.project.task_count}</p>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<div className="grid gap-3 sm:grid-cols-2 xl:col-span-7 xl:grid-cols-3">
|
||
<Card>
|
||
<CardHeader className="pb-2">
|
||
<CardTitle className="text-base">全局认知对齐度</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-2">
|
||
<div className="h-20">
|
||
<ResponsiveContainer width="100%" height="100%">
|
||
<PieChart>
|
||
<Pie
|
||
data={[
|
||
{ name: "score", value: Math.max(0, Math.min(100, overview.alignment_score || 0)) },
|
||
{ name: "rest", value: Math.max(0, 100 - (overview.alignment_score || 0)) },
|
||
]}
|
||
innerRadius={22}
|
||
outerRadius={34}
|
||
dataKey="value"
|
||
startAngle={90}
|
||
endAngle={-270}
|
||
stroke="none"
|
||
>
|
||
<Cell fill={alignmentColor} />
|
||
<Cell fill="hsl(var(--muted))" />
|
||
</Pie>
|
||
</PieChart>
|
||
</ResponsiveContainer>
|
||
</div>
|
||
<p className="text-lg font-semibold sm:text-xl" style={{ color: alignmentColor }}>
|
||
{Math.round(overview.alignment_score || 0)}%
|
||
</p>
|
||
<p className="text-xs text-muted-foreground">
|
||
未确认节点 {overview.pending_alignment_nodes} | 冲突 {overview.unresolved_conflicts}
|
||
</p>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card>
|
||
<CardHeader className="pb-2">
|
||
<CardTitle className="text-base">项目整体进度</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-2">
|
||
<p className="text-xl font-semibold sm:text-2xl">{Math.round(progress)}%</p>
|
||
<div className="h-2 overflow-hidden rounded-full bg-muted">
|
||
<div className="h-full bg-primary" style={{ width: `${Math.min(progress, 100)}%` }} />
|
||
</div>
|
||
<p className="text-xs text-muted-foreground">
|
||
已完成 {overview.task_progress.done}/{overview.task_progress.total}
|
||
</p>
|
||
<p className="text-xs text-muted-foreground">
|
||
延期 {overview.task_progress.overdue} | 阻塞 {overview.task_progress.blocked} | 今日完成 {overview.task_progress.today_done}
|
||
</p>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card className="cursor-pointer transition-colors hover:bg-accent/40" onClick={onOpenWorkloads}>
|
||
<CardHeader className="pb-2">
|
||
<CardTitle className="text-base">团队负载健康度</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-3">
|
||
<p className="text-xl font-semibold sm:text-2xl" style={{ color: ratioColor(ratio) }}>
|
||
{(ratio * 100).toFixed(0)}%
|
||
</p>
|
||
<div className="h-2 overflow-hidden rounded-full bg-muted">
|
||
<div
|
||
className="h-full"
|
||
style={{
|
||
width: `${Math.min(ratio * 100, 100)}%`,
|
||
backgroundColor: ratioColor(ratio),
|
||
}}
|
||
/>
|
||
</div>
|
||
<p className="text-xs text-muted-foreground">
|
||
过载 {overview.workload_health.overloaded_count} 人 | 空闲 {overview.workload_health.idle_count} 人
|
||
</p>
|
||
<p className="inline-flex items-center gap-1 text-xs text-muted-foreground">
|
||
<Users className="h-3 w-3" />
|
||
覆盖成员 {overview.workload_health.member_count} 人
|
||
</p>
|
||
<div className="space-y-1.5 rounded-lg border border-slate-200/70 bg-slate-50/70 p-2">
|
||
{workloadMembers.length === 0 ? (
|
||
<p className="text-xs text-slate-500">暂无成员负载明细,请先分配项目成员</p>
|
||
) : (
|
||
workloadMembers.slice(0, 4).map((member) => (
|
||
<div
|
||
key={member.user_id}
|
||
className="flex items-center justify-between gap-2 rounded-md bg-white px-2 py-1 shadow-[0_1px_2px_rgba(15,23,42,0.06)]"
|
||
>
|
||
<div className="min-w-0">
|
||
<p className="truncate text-xs font-semibold text-slate-800">{member.name}</p>
|
||
<p className="truncate text-[11px] text-slate-500">{normalizeRoleLabel(member.role)}</p>
|
||
</div>
|
||
<div className="flex items-center gap-1.5">
|
||
<span className={`text-xs font-semibold ${ratioClass(Number(member.workload_ratio || 0))}`}>
|
||
{(Number(member.workload_ratio || 0) * 100).toFixed(0)}%
|
||
</span>
|
||
<span
|
||
className={`rounded-full px-1.5 py-0.5 text-[10px] font-medium ${memberStatusClass(member.status)}`}
|
||
>
|
||
{memberStatusLabel(member.status)}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
))
|
||
)}
|
||
{workloadMembers.length > 4 ? (
|
||
<p className="text-[11px] text-slate-500">其余 {workloadMembers.length - 4} 人请点击查看完整负载</p>
|
||
) : null}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
<Card className="xl:col-span-2">
|
||
<CardHeader className="pb-2">
|
||
<CardTitle className="text-base">核心操作</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-2">
|
||
<Button className="w-full" onClick={onOpenAi}>
|
||
<Sparkles className="mr-2 h-4 w-4" />
|
||
AI 项目管家
|
||
</Button>
|
||
<Button variant="outline" className="w-full" onClick={onOpenSettings}>
|
||
<Settings className="mr-2 h-4 w-4" />
|
||
项目设置
|
||
</Button>
|
||
<Button variant="ghost" className="w-full" onClick={onExport}>
|
||
<Download className="mr-2 h-4 w-4" />
|
||
导出链路文档
|
||
</Button>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
);
|
||
}
|