import { useQueryClient } from "@tanstack/react-query"; import type { Decision, DecisionKind } from "@umt/contracts"; import { ArrowRight, Bell, CheckCheck } from "lucide-react"; import { useMemo, useRef, useState } from "react"; import { Link, useNavigate } from "react-router"; import { decisionKeys, useDecisionSummary, useDecisions } from "~/api/decisions.ts"; import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx"; import { SEVERITY } from "~/components/library/labels.ts"; import { Button } from "~/components/ui/button.tsx"; import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover.tsx"; import { decisionKindLabel, formatRelative } from "~/lib/labels.ts"; import { useSse } from "~/lib/sse.ts"; import { cn } from "~/lib/utils.ts"; // 顶栏收件箱(IA-3):待处理不进侧栏,铃铛 + 未决角标;下拉按问题类型列最近事项,点开进待处理页。 // 角标只算需要你处理的事项(警告及以上):"仅信息"类(巡检报告、旧版本待处置等)不计入,在待处理页可展开查看。 // 身份类事项已按来源 + 标题合并,一条事项可能代表多条资源(标题里写明 N 条)。 const RECENT = 8; const DOT = { info: "bg-muted-foreground/60", warning: "bg-warning", critical: "bg-destructive" } as const; /** 未决数与最近事项;SSE decision.* 到达时刷新 */ export function useLiveDecisions() { const qc = useQueryClient(); const timer = useRef | null>(null); useSse(["decision.created", "decision.updated"], () => { if (timer.current) return; timer.current = setTimeout(() => { timer.current = null; void qc.invalidateQueries({ queryKey: decisionKeys.all }); }, 800); }); return useDecisionSummary(undefined, "warning"); } export const inboxPath = (kind?: string) => (kind ? `/console/inbox?kind=${kind}` : "/console/inbox"); export function InboxBell() { const summary = useLiveDecisions(); const [open, setOpen] = useState(false); const list = useDecisions({ minSeverity: "warning" }, open); const navigate = useNavigate(); const total = summary.data?.total ?? 0; const groups = useMemo(() => { const items = (list.data?.pages[0]?.items ?? []).slice(0, RECENT); const m = new Map(); for (const d of items) m.set(d.kind, [...(m.get(d.kind) ?? []), d]); const counts = new Map(); for (const g of summary.data?.groups ?? []) counts.set(g.kind, (counts.get(g.kind) ?? 0) + g.count); return [...m.entries()].map(([kind, ds]) => ({ kind, items: ds, count: counts.get(kind) ?? ds.length })); }, [list.data, summary.data]); const go = (to: string) => { setOpen(false); navigate(to); }; return (

待处理 {total ? {total} : null}

{list.isPending ? ( ) : list.isError ? ( ) : groups.length === 0 ? ( ) : (
{groups.map((g) => (
    {g.items.map((d) => (
  • setOpen(false)} className="block rounded-lg px-2 py-2 outline-none transition-colors hover:bg-foreground/[0.06] focus-visible:bg-foreground/[0.06] focus-visible:ring-2 focus-visible:ring-ring/50" > {d.title} {SEVERITY[d.severity].label} · {formatRelative(d.createdAt)}
  • ))}
))}
)}
); }