// 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 (
);
}
// 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)}
平均响应
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.lastMsg}」
{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
});