// Shared atomic components (avatars, pills, sparklines, helpers) const { useState, useMemo, useEffect, useRef } = React; function fmtMin(min) { if (min == null || isNaN(min)) return "—"; if (min < 1) return "刚刚"; if (min < 60) return `${min}分钟前`; if (min < 1440) return `${Math.floor(min / 60)}小时前`; return `${Math.floor(min / 1440)}天前`; } function fmtDuration(min) { if (min < 60) return `${min}分钟`; if (min < 1440) { const h = Math.floor(min / 60), m = min % 60; return m ? `${h}时${m}分` : `${h}小时`; } const d = Math.floor(min / 1440), h = Math.floor((min % 1440) / 60); return h ? `${d}天${h}时` : `${d}天`; } function Avatar({ dr, size = "md" }) { if (!dr) return null; const sz = size === "lg" ? 36 : size === "sm" ? 22 : 28; const cls = size === "lg" ? "avatar" : size === "sm" ? "avatar avatar-sm" : "avatar avatar-md"; const bg = `oklch(0.78 0.12 ${dr.hue})`; return (
{dr.initial}
); } function StatusPill({ status, count, label }) { const map = { critical: { cls: "pill-critical", txt: "紧急" }, warning: { cls: "pill-warning", txt: "告警" }, silent: { cls: "pill-silent", txt: "沉默" }, healthy: { cls: "pill-healthy", txt: "正常" }, }; const m = map[status] || map.healthy; return ( {label || m.txt} {count != null && {count}} ); } // SVG sparkline function Sparkline({ data, width = 60, height = 24, color = "currentColor", fill = false }) { if (!data || !data.length) return null; const max = Math.max(...data, 1); const min = Math.min(...data, 0); const w = width, h = height; const pts = data.map((v, i) => { const x = (i / (data.length - 1)) * w; const y = h - ((v - min) / (max - min || 1)) * (h - 2) - 1; return `${x.toFixed(1)},${y.toFixed(1)}`; }).join(" "); return ( {fill && ( )} ); } // Health bar split into 4 segments by status function HealthBar({ critical, warning, silent, healthy }) { const total = critical + warning + silent + healthy || 1; return (
{critical > 0 && } {warning > 0 && } {silent > 0 && } {healthy > 0 && }
); } function GroupRow({ group, onClick }) { const dr = MOCK.DRS.find(d => d.id === group.drId); return (
{group.name} {group.status !== "healthy" && ( )}
{group.lastMsgKind === "inbound" ? `${group.customerName}:` : "DR:"} {group.lastMsg}
{fmtMin(group.lastInboundMinutes)}
); } function DRCard({ dr, density, onOpen, onOpenGroup }) { const groups = MOCK.groupsForDr(dr.id); const visible = density === "compact" ? groups.slice(0, 3) : groups.slice(0, 5); const hidden = groups.length - visible.length; const hasCritical = dr.critical > 0; return (
onOpen(dr)}>
{dr.name} {hasCritical && }
活跃 {dr.activeGroups} / 总计 {dr.totalGroups} 群 · 最近 {fmtMin(dr.lastActiveMinutes)}
{dr.score}
健康度
今日消息
{dr.todayMessages}
回复数
{dr.todayReplies}
平均响应
60 ? "warn" : ""}`}> {dr.avgResponseMin}
超时群
0 ? "danger" : ""}`}> {dr.overdueGroups}
关注列表 · {groups.filter(g => g.status !== "healthy").length} 项需处理
{visible.map(g => ( onOpenGroup(g)} /> ))} {hidden > 0 && (
还有 {hidden} 个群 · 点击 DR 查看全部
)} {visible.length === 0 && (
无关注项 · 状态良好
)}
); } function AlertRow({ alert, onClick }) { const dr = MOCK.DRS.find(d => d.id === alert.drId); const g = alert.group; return (
{g.name}
{alert.label}
「{g.lastMsg}」
{dr.name}
{fmtMin(g.lastInboundMinutes)}
); } // Drawer for DR detail and group conversation function Drawer({ open, onClose, children, title, subtitle, headExtra }) { useEffect(() => { if (!open) return; const onKey = (e) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [open, onClose]); if (!open) return null; return ( <>
{headExtra}
{title}
{subtitle &&
{subtitle}
}
{children}
); } // Stat card with optional sparkline function StatCard({ label, value, unit, sub, alertLevel, sparkData, sparkColor }) { return (
{label}
{value} {unit && {unit}}
{sub &&
{sub}
} {sparkData && (
)}
); } Object.assign(window, { fmtMin, fmtDuration, Avatar, StatusPill, Sparkline, HealthBar, GroupRow, DRCard, AlertRow, Drawer, StatCard });