PMFlow/frontend/src/components/sstc/SSTCRiskFloatPanel.tsx

123 lines
4.5 KiB
TypeScript

import { useMemo } from "react";
import { AlertTriangle, Brain, Maximize2 } from "lucide-react";
import type { SSTCRisks } from "@/api/sstc";
import { Button } from "@/components/ui/button";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
interface SSTCRiskPanelProps {
risks: SSTCRisks;
onJump?: (entityId: string) => void;
}
function formatBadgeCount(total: number): string {
return total > 99 ? "99+" : String(total);
}
function computeRiskTotal(risks: SSTCRisks): number {
return (risks.summary.conflict_count || 0) + (risks.summary.risk_count || 0);
}
export function SSTCRiskCenterPanel({ risks, onJump }: SSTCRiskPanelProps) {
const total = useMemo(
() => computeRiskTotal(risks),
[risks]
);
return (
<div className="w-[min(360px,calc(100vw-1rem))] rounded-lg bg-card">
<div className="flex items-center justify-between border-b px-3 py-2">
<p className="text-sm font-semibold">项目预警中心</p>
<div className="flex items-center gap-2">
<span className="rounded-full bg-red-600 px-2 py-0.5 text-[10px] text-white">
{formatBadgeCount(total)}
</span>
</div>
</div>
<div className="p-2">
<Tabs defaultValue="conflicts">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="conflicts">认知冲突</TabsTrigger>
<TabsTrigger value="risks">风险预警</TabsTrigger>
</TabsList>
<TabsContent value="conflicts" className="space-y-2">
{risks.conflicts.length === 0 ? (
<p className="px-1 py-4 text-center text-xs text-muted-foreground">暂无未解决冲突</p>
) : (
risks.conflicts.map((item) => (
<div key={item.id} className="rounded-md border p-2">
<p className="line-clamp-2 text-xs">{item.title}</p>
<div className="mt-2 flex justify-end">
<Button
size="sm"
variant="outline"
className="h-7 text-xs"
onClick={() => onJump?.(item.entity_id)}
>
<Brain className="mr-1 h-3 w-3" />
一键处理
</Button>
</div>
</div>
))
)}
</TabsContent>
<TabsContent value="risks" className="space-y-2">
{risks.risks.length === 0 ? (
<p className="px-1 py-4 text-center text-xs text-muted-foreground">暂无风险预警</p>
) : (
risks.risks.map((item) => (
<div key={item.id} className="rounded-md border p-2">
<p className="line-clamp-2 text-xs">{item.title}</p>
<div className="mt-2 flex justify-end">
<Button
size="sm"
variant="outline"
className="h-7 text-xs"
onClick={() => onJump?.(item.entity_id)}
>
<Maximize2 className="mr-1 h-3 w-3" />
查看详情
</Button>
</div>
</div>
))
)}
</TabsContent>
</Tabs>
<Button className="mt-2 w-full" size="sm">
AI 一键处理所有预警
</Button>
</div>
</div>
);
}
export function SSTCRiskTopbarEntry({ risks, onJump }: SSTCRiskPanelProps) {
const total = useMemo(() => computeRiskTotal(risks), [risks]);
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="relative h-9 gap-1.5 rounded-full px-3 text-slate-700 hover:bg-slate-100"
title="打开预警中心"
>
<AlertTriangle className="h-4 w-4" />
<span className="text-sm font-medium">预警</span>
{total > 0 && (
<span className="absolute -right-0.5 -top-0.5 min-w-4 rounded-full bg-red-500 px-1 text-center text-[10px] leading-4 text-white">
{formatBadgeCount(total)}
</span>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[min(360px,calc(100vw-1rem))] rounded-lg border p-0">
<SSTCRiskCenterPanel risks={risks} onJump={onJump} />
</DropdownMenuContent>
</DropdownMenu>
);
}