feat(integration): 接入 1006 辆真实车辆数据 + 24h 异常派生 + 重点品牌锚定
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

数据
- data/vehicles-real.js: 从 车辆管理-1167382387645005824.xlsx 抽取 1006 辆,含车牌/VIN/品牌/型号/部门/客户/合同等真实字段
- 接入分布按业务要求: GB32960 在线 700 / JT808 在线 950 / 完全未对接 30
- 业务规则: GB 未对接 226 辆 = 苏龙 197 + 海伯特 29(从帕力安牌中重命名 29 辆)
- DEPARTMENTS 扩展 biz5 / biz6 以匹配 xlsx 部门
- fleet.js 优先使用 RAW_VEHICLES,前 12 辆叠加乍浦港地图坐标

派生状态
- integration.jsx 新增 effectiveStatus(): offline 且 lastSeen ≥ 24h → abnormal (异常)
- ConnPill 增 abnormal 红色态; 重点标记 = 双通道均 abnormal/未对接
- 筛选/KPI 全部基于派生状态实时计算(每 30s 重算)
This commit is contained in:
kkfluous
2026-04-28 15:57:14 +08:00
parent e38bd8a1d8
commit 97a2f54786
5 changed files with 141 additions and 36 deletions

View File

@@ -28,11 +28,26 @@ const fmtDate = (ts) => {
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:"断流" },
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;
@@ -100,22 +115,30 @@ const ArtboardIntegration = () => {
}, []);
// 派生重点标记 + 状态等级(用于排序)
// tick 让 useMemo 在定时器触发时重算时间相关状态
const enriched = React.useMemo(() => allVehicles.map(v => {
const isCritical = v.gbStatus === "not_connected" && v.jtStatus === "not_connected";
const hasOffline = v.gbStatus === "offline" || v.jtStatus === "offline";
const allOnline = v.gbStatus === "online" && v.jtStatus === "online";
let priority = 4;
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 (v.gbStatus === "not_connected" || v.jtStatus === "not_connected") priority = 1;
else if (hasOffline) priority = 2;
else if (allOnline) priority = 3;
return { ...v, isCritical, hasOffline, allOnline, priority };
}), [allVehicles]);
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.gbStatus === "online" || v.jtStatus === "online")) return false;
if (filterMode === "online" && !(v.gbEff === "online" || v.jtEff === "online")) return false;
if (filterMode === "offline" && !v.hasOffline) return false;
if (filterMode === "not_connected" && !(v.gbStatus === "not_connected" || v.jtStatus === "not_connected")) 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();
@@ -135,12 +158,33 @@ const ArtboardIntegration = () => {
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: enriched.filter(v => v.gbStatus === "online" || v.jtStatus === "online").length, tone: "ok" },
{ k: "offline", l: "断流", c: enriched.filter(v => v.hasOffline).length, tone: "warn" },
{ k: "not_connected", l: "未对接", c: enriched.filter(v => v.gbStatus === "not_connected" || v.jtStatus === "not_connected").length, tone: "danger" },
{ k: "both_none", l: "完全未对接", c: enriched.filter(v => v.isCritical).length, tone: "danger" },
{ 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 (
@@ -150,15 +194,15 @@ const ArtboardIntegration = () => {
<Topbar
crumbs={["羚牛车辆数据中心", "数据接入", "监控总览"]}
kpis={[
{ lbl:"总车辆", val: counts.all },
{ lbl:"GB在线", val: counts.gbOnline },
{ lbl:"JT在线", val: counts.jtOnline },
{ lbl:"重点未对接", val: counts.bothNone, deltaUp:false, delta: counts.bothNone > 0 ? "需处理" : "—" },
{ lbl:"总车辆", val: enriched.length },
{ lbl:"GB在线", val: derivedCounts.gbOnline },
{ lbl:"JT在线", val: derivedCounts.jtOnline },
{ lbl:"完全未对接", val: derivedCounts.bothRed, deltaUp:false, delta: derivedCounts.bothRed > 0 ? "需处理" : "—" },
]}
/>
{/* 顶部 banner — 如果有完全未对接车辆,全屏告警条 */}
{counts.bothNone > 0 && (
{derivedCounts.bothRed > 0 && (
<div style={{
padding:"10px 18px",
background:"linear-gradient(90deg, var(--danger-soft), transparent)",
@@ -170,7 +214,7 @@ const ArtboardIntegration = () => {
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0Z"/>
<line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>
</svg>
<span>检测到 <span className="strong" style={{color:"var(--danger)", fontFamily:"var(--font-mono)", fontWeight:600}}>{counts.bothNone}</span> 辆车 GB32960 JT808 双协议均未对接可能为新增/更换车机后未配置上行 请尽快在对应业务系统下工单核查</span>
<span>检测到 <span className="strong" style={{color:"var(--danger)", fontFamily:"var(--font-mono)", fontWeight:600}}>{derivedCounts.bothRed}</span> 辆车 GB32960 JT808 双协议均处于"未对接 / 超 24h 未上报"状态可能为新增/更换车机后未配置上行 请尽快在对应业务系统下工单核查</span>
<button className="btn" style={{marginLeft:"auto", height:24, padding:"0 10px", fontSize:11}} onClick={() => setFilterMode("both_none")}>
查看全部
</button>
@@ -179,12 +223,12 @@ const ArtboardIntegration = () => {
{/* KPI 行 */}
<div style={{display:"flex", gap:12, padding:"16px 18px 0"}}>
<KpiCard label="总车辆" value={counts.all} tone="neutral" accent/>
<KpiCard label="GB32960 接入" value={counts.gbOnline} sub={`/ ${counts.all}`} tone="ok" accent/>
<KpiCard label="GB32960 断流" value={counts.gbOffline} tone="warn" accent/>
<KpiCard label="JT808/1078" value={counts.jtOnline} sub={`/ ${counts.all}`} tone="ok" accent/>
<KpiCard label="JT 断流" value={counts.jtOffline} tone="warn" accent/>
<KpiCard label="完全未对接" value={counts.bothNone} sub={counts.bothNone > 0 ? "重点处理" : "—"} tone="danger" accent/>
<KpiCard label="总车辆" value={enriched.length} tone="neutral" accent/>
<KpiCard label="GB32960 接入" value={derivedCounts.gbOnline} sub={`/ ${enriched.length}`} tone="ok" accent/>
<KpiCard label="GB 异常 ≥24h" value={derivedCounts.gbAbnormal + derivedCounts.gbNotConn} tone="danger" accent/>
<KpiCard label="JT808/1078" value={derivedCounts.jtOnline} sub={`/ ${enriched.length}`} tone="ok" accent/>
<KpiCard label="JT 异常 ≥24h" value={derivedCounts.jtAbnormal + derivedCounts.jtNotConn} tone="danger" accent/>
<KpiCard label="完全未对接" value={derivedCounts.bothRed} sub={derivedCounts.bothRed > 0 ? "重点处理" : "—"} tone="danger" accent/>
</div>
{/* 工具栏 */}
@@ -298,9 +342,9 @@ const ArtboardIntegration = () => {
</div>
</td>
<td>
<ConnPill status={v.gbStatus} protocol="GB"/>
<ConnPill status={v.gbEff} protocol="GB"/>
</td>
<td className="mono" style={{color: v.gbStatus === "not_connected" ? "var(--fg-3)" : v.gbStatus === "offline" ? "var(--warn)" : "var(--fg-1)"}}>
<td className="mono" style={{color: v.gbEff === "not_connected" ? "var(--fg-3)" : v.gbEff === "abnormal" ? "var(--danger)" : v.gbEff === "offline" ? "var(--warn)" : "var(--fg-1)"}}>
{v.gbLastSeen ? (
<div style={{display:"flex", flexDirection:"column", gap:1, fontSize:11}}>
<span>{fmtRelative(v.gbLastSeen)}</span>
@@ -309,9 +353,9 @@ const ArtboardIntegration = () => {
) : <span style={{color:"var(--fg-3)"}}></span>}
</td>
<td>
<ConnPill status={v.jtStatus} protocol="JT"/>
<ConnPill status={v.jtEff} protocol="JT"/>
</td>
<td className="mono" style={{color: v.jtStatus === "not_connected" ? "var(--fg-3)" : v.jtStatus === "offline" ? "var(--warn)" : "var(--fg-1)"}}>
<td className="mono" style={{color: v.jtEff === "not_connected" ? "var(--fg-3)" : v.jtEff === "abnormal" ? "var(--danger)" : v.jtEff === "offline" ? "var(--warn)" : "var(--fg-1)"}}>
{v.jtLastSeen ? (
<div style={{display:"flex", flexDirection:"column", gap:1, fontSize:11}}>
<span>{fmtRelative(v.jtLastSeen)}</span>