// integration.jsx — 数据接入监控 // 后端数据管理总览:所有车辆的 GB32960 / JT808+1078 对接情况、最后接收时间 // 完全未对接的车辆重点标记,便于发现新车未接入或数据停止上报。 const fmtRelative = (ts) => { if (!ts) return "—"; const diff = Date.now() - ts; const m = Math.floor(diff / 60000); if (m < 1) return "刚刚"; if (m < 60) return m + " 分钟前"; const h = Math.floor(m / 60); if (h < 24) return h + " 小时前"; const d = Math.floor(h / 24); return d + " 天前"; }; const fmtAbsolute = (ts) => { if (!ts) return "—"; const d = new Date(ts); const pad = (n) => String(n).padStart(2, "0"); return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; }; const fmtDate = (ts) => { if (!ts) return "—"; const d = new Date(ts); const pad = (n) => String(n).padStart(2, "0"); return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`; }; // 24h 未上报阈值 const STALE_THRESHOLD = 24 * 60 * 60 * 1000; // 派生有效状态:offline 且 last_seen > 24h → abnormal(异常) const effectiveStatus = (rawStatus, lastSeen) => { if (rawStatus === "online" || rawStatus === "not_connected") return rawStatus; // offline if (lastSeen && Date.now() - lastSeen > STALE_THRESHOLD) return "abnormal"; return "offline"; }; // 是否为"红色重点"状态(需要业务跟进) const isRedState = (st) => st === "abnormal" || st === "not_connected"; // Status pill const ConnPill = ({ status, protocol }) => { const map = { online: { bg:"var(--accent-soft)", fg:"var(--accent)", bd:"rgba(0,113,67,0.30)", dot:"var(--accent)", l:"在线" }, offline: { bg:"var(--warn-soft)", fg:"var(--warn)", bd:"rgba(181,122,14,0.35)", dot:"var(--warn)", l:"断流 <24h" }, abnormal: { bg:"var(--danger-soft)", fg:"var(--danger)", bd:"rgba(179,48,40,0.40)", dot:"var(--danger)", l:"异常 ≥24h" }, not_connected: { bg:"var(--danger-soft)", fg:"var(--danger)", bd:"rgba(179,48,40,0.40)", dot:"var(--danger)", l:"未对接" }, }; const m = map[status] || map.not_connected; return ( {protocol && {protocol}} {m.l} ); }; const KpiCard = ({ label, value, sub, tone, accent }) => { const toneMap = { ok: { fg:"var(--accent)", bg:"var(--accent-soft)", bd:"rgba(0,113,67,0.20)" }, warn: { fg:"var(--warn)", bg:"var(--warn-soft)", bd:"rgba(181,122,14,0.25)" }, danger: { fg:"var(--danger)", bg:"var(--danger-soft)", bd:"rgba(179,48,40,0.30)" }, neutral:{ fg:"var(--fg-0)", bg:"var(--bg-2)", bd:"var(--border-1)" }, }; const t = toneMap[tone] || toneMap.neutral; return (
{accent &&
}
{label}
{value} {sub && {sub}}
); }; const ArtboardIntegration = () => { const allVehicles = (window.VEHICLES || []); const counts = (window.COUNTS || {}); const [filterMode, setFilterMode] = React.useState("all"); // all | online | offline | not_connected | both_none const [search, setSearch] = React.useState(""); const [sortBy, setSortBy] = React.useState("priority"); // priority | plate | gbTime | jtTime const [tick, setTick] = React.useState(0); // 实时刷新相对时间显示 React.useEffect(() => { const id = setInterval(() => setTick((x) => x + 1), 30000); return () => clearInterval(id); }, []); // 派生重点标记 + 状态等级(用于排序) // tick 让 useMemo 在定时器触发时重算时间相关状态 const enriched = React.useMemo(() => allVehicles.map(v => { const gbEff = effectiveStatus(v.gbStatus, v.gbLastSeen); const jtEff = effectiveStatus(v.jtStatus, v.jtLastSeen); // 重点标记:双通道都是红色状态(异常 或 未对接) const isCritical = isRedState(gbEff) && isRedState(jtEff); const hasOffline = gbEff === "offline" || jtEff === "offline"; const hasAbnormal = gbEff === "abnormal" || jtEff === "abnormal"; const hasNotConn = gbEff === "not_connected" || jtEff === "not_connected"; const allOnline = gbEff === "online" && jtEff === "online"; let priority = 5; if (isCritical) priority = 0; else if (hasNotConn) priority = 1; else if (hasAbnormal) priority = 2; else if (hasOffline) priority = 3; else if (allOnline) priority = 4; return { ...v, gbEff, jtEff, isCritical, hasOffline, hasAbnormal, hasNotConn, allOnline, priority }; // eslint-disable-next-line react-hooks/exhaustive-deps }), [allVehicles, tick]); const filtered = enriched.filter(v => { if (filterMode === "online" && !(v.gbEff === "online" || v.jtEff === "online")) return false; if (filterMode === "offline" && !v.hasOffline) return false; if (filterMode === "abnormal" && !(v.hasAbnormal || v.hasNotConn)) return false; if (filterMode === "both_none" && !v.isCritical) return false; if (search) { const q = search.toLowerCase(); if (!v.plate.toLowerCase().includes(q) && !v.vin.toLowerCase().includes(q) && !v.brand.toLowerCase().includes(q) && !v.model.toLowerCase().includes(q)) return false; } return true; }); const sorted = [...filtered].sort((a, b) => { if (sortBy === "priority") return a.priority - b.priority; if (sortBy === "plate") return a.plate.localeCompare(b.plate); if (sortBy === "gbTime") return (b.gbLastSeen || 0) - (a.gbLastSeen || 0); if (sortBy === "jtTime") return (b.jtLastSeen || 0) - (a.jtLastSeen || 0); return 0; }); // 派生计数(基于 effectiveStatus) const derivedCounts = React.useMemo(() => { const c = { gbOnline:0, gbOffline:0, gbAbnormal:0, gbNotConn:0, jtOnline:0, jtOffline:0, jtAbnormal:0, jtNotConn:0, anyOnline:0, hasOffline:0, hasAbnormalOrNC:0, bothRed:0 }; enriched.forEach(v => { if (v.gbEff === "online") c.gbOnline++; else if (v.gbEff === "offline") c.gbOffline++; else if (v.gbEff === "abnormal") c.gbAbnormal++; else c.gbNotConn++; if (v.jtEff === "online") c.jtOnline++; else if (v.jtEff === "offline") c.jtOffline++; else if (v.jtEff === "abnormal") c.jtAbnormal++; else c.jtNotConn++; if (v.gbEff === "online" || v.jtEff === "online") c.anyOnline++; if (v.hasOffline) c.hasOffline++; if (v.hasAbnormal || v.hasNotConn) c.hasAbnormalOrNC++; if (v.isCritical) c.bothRed++; }); return c; // eslint-disable-next-line react-hooks/exhaustive-deps }, [enriched, tick]); const filters = [ { k: "all", l: "全部", c: enriched.length, tone: "neutral" }, { k: "online", l: "任一在线", c: derivedCounts.anyOnline, tone: "ok" }, { k: "offline", l: "断流 <24h", c: derivedCounts.hasOffline, tone: "warn" }, { k: "abnormal", l: "异常 ≥24h", c: derivedCounts.hasAbnormalOrNC, tone: "danger" }, { k: "both_none", l: "完全未对接", c: derivedCounts.bothRed, tone: "danger" }, ]; return (
0 ? "需处理" : "—" }, ]} /> {/* 顶部 banner — 如果有完全未对接车辆,全屏告警条 */} {derivedCounts.bothRed > 0 && (
检测到 {derivedCounts.bothRed} 辆车 GB32960 与 JT808 双协议均处于"未对接 / 超 24h 未上报"状态,可能为新增/更换车机后未配置上行 — 请尽快在对应业务系统下工单核查。
)} {/* KPI 行 */}
0 ? "重点处理" : "—"} tone="danger" accent/>
{/* 工具栏 */}
setSearch(e.target.value)}/>
{filters.map(f => ( setFilterMode(f.k)} className="chip" style={{ cursor:"pointer", fontSize:11, height:26, padding:"0 10px", background: filterMode === f.k ? (f.tone === "danger" ? "var(--danger-soft)" : f.tone === "warn" ? "var(--warn-soft)" : f.tone === "ok" ? "var(--accent-soft)" : "var(--bg-3)") : "var(--bg-2)", color: filterMode === f.k ? (f.tone === "danger" ? "var(--danger)" : f.tone === "warn" ? "var(--warn)" : f.tone === "ok" ? "var(--accent)" : "var(--fg-0)") : "var(--fg-2)", border: "1px solid " + (filterMode === f.k ? (f.tone === "danger" ? "rgba(179,48,40,0.30)" : f.tone === "warn" ? "rgba(181,122,14,0.30)" : f.tone === "ok" ? "rgba(0,113,67,0.25)" : "var(--border-2)") : "var(--border-1)"), fontWeight: filterMode === f.k ? 500 : 400, }}> {f.l} {f.c} ))}
排序
{/* 表格 */}
{sorted.map(v => { const flagged = v.isCritical; return ( ); })} {sorted.length === 0 && ( )}
车牌 VIN / 车架号 品牌 / 型号 GB32960 GB32960 最后接收 JT808 / 1078 JT 最后接收 接入时间
{flagged ? ( ) : v.hasOffline ? ( ) : null} {v.plate} {v.vin}
{v.brand} {v.model}
{v.gbLastSeen ? (
{fmtRelative(v.gbLastSeen)} {fmtAbsolute(v.gbLastSeen)}
) : }
{v.jtLastSeen ? (
{fmtRelative(v.jtLastSeen)} {fmtAbsolute(v.jtLastSeen)}
) : }
{/* 完全未对接的车辆从未接入,不显示接入时间 */} {flagged ? : fmtDate(v.onboardAt)}
没有匹配的车辆
{sorted.length} 辆车 · 数据每 30 秒刷新一次 处理流程:发现未对接 → 在车联网平台下工单 → 配置 TBOX/JT 主机参数 → 回流验证
); }; window.ArtboardIntegration = ArtboardIntegration;