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 { 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):待处理不进侧栏,铃铛 + 未决角标;下拉按问题类型列最近事项,点开进待处理页。 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(); } 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({}, 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="flex items-start gap-2.5 px-4 py-2 hover:bg-foreground/[0.04]" > {d.title} {SEVERITY[d.severity].label} · {formatRelative(d.createdAt)}
  • ))}
))}
)}
); }