123 lines
4.5 KiB
TypeScript
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>
|
|
);
|
|
}
|