feat(integration): 新增数据接入监控页
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- 字段:车牌 / VIN / 品牌+型号 / GB32960 状态+最后接收 / JT808 状态+最后接收 / 接入时间 - 状态:在线 / 断流 / 未对接,三色 pill + 脉冲点 - 重点标记:双协议均未对接的车辆 → 行红底 + 警示图标 + 顶部 banner - 工具栏:搜索(车牌/VIN/品牌/型号)+ 5 维筛选 + 4 维排序 + CSV 导出 - KPI:总车辆 / GB 在线 / GB 断流 / JT 在线 / JT 断流 / 完全未对接 - 数据:fleet.js 增 brand/model/gbStatus/gbLastSeen/jtStatus/jtLastSeen/onboardAt - 路由 #/integration · sidebar 增 plug 图标项
This commit is contained in:
5
app.jsx
5
app.jsx
@@ -6,8 +6,9 @@ const ROUTES = [
|
||||
{ path: "detail", icon: "car", label: "车辆详情", crumbs: ["羚牛车辆数据中心", "实时监控", "单车详情"], component: "ArtboardDetail" },
|
||||
{ path: "history", icon: "history", label: "历史查询", crumbs: ["羚牛车辆数据中心", "数据分析", "历史查询"], component: "ArtboardHistory" },
|
||||
{ path: "playback", icon: "route", label: "轨迹回放", crumbs: ["羚牛车辆数据中心", "数据分析", "轨迹回放"], component: "ArtboardPlayback" },
|
||||
{ path: "alarm", icon: "bell", label: "事件规则", crumbs: ["羚牛车辆数据中心", "事件中心", "规则编排"], component: "ArtboardAlarm" },
|
||||
{ path: "inbox", icon: "inbox", label: "通知中心", crumbs: ["羚牛车辆数据中心", "事件中心", "通知中心"], component: "ArtboardInbox" },
|
||||
{ path: "alarm", icon: "bell", label: "事件规则", crumbs: ["羚牛车辆数据中心", "事件中心", "规则编排"], component: "ArtboardAlarm" },
|
||||
{ path: "inbox", icon: "inbox", label: "通知中心", crumbs: ["羚牛车辆数据中心", "事件中心", "通知中心"], component: "ArtboardInbox" },
|
||||
{ path: "integration", icon: "plug", label: "数据接入监控", crumbs: ["羚牛车辆数据中心", "数据接入", "监控总览"], component: "ArtboardIntegration" },
|
||||
];
|
||||
|
||||
const SUB_ROUTES = [
|
||||
|
||||
350
artboards/integration.jsx
Normal file
350
artboards/integration.jsx
Normal file
@@ -0,0 +1,350 @@
|
||||
// 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())}`;
|
||||
};
|
||||
|
||||
// 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:"断流" },
|
||||
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 (
|
||||
<span style={{
|
||||
display:"inline-flex", alignItems:"center", gap:5,
|
||||
height:21, padding:"0 8px",
|
||||
fontSize:11, fontWeight:500,
|
||||
borderRadius:4,
|
||||
background:m.bg, color:m.fg, border:"1px solid "+m.bd,
|
||||
whiteSpace:"nowrap",
|
||||
}}>
|
||||
<span style={{
|
||||
width:6, height:6, borderRadius:3, background:m.dot,
|
||||
boxShadow: status === "online" ? "0 0 4px " + m.dot : "none",
|
||||
animation: status === "online" ? "pulse 1.6s ease-in-out infinite" : "none",
|
||||
}}/>
|
||||
{protocol && <span style={{fontFamily:"var(--font-mono)", fontSize:9, opacity:.7, marginRight:2}}>{protocol}</span>}
|
||||
{m.l}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<div style={{
|
||||
flex:1, minWidth:160,
|
||||
padding:"14px 16px",
|
||||
background:"var(--bg-1)",
|
||||
border:"1px solid var(--border-1)",
|
||||
borderRadius:8,
|
||||
position:"relative",
|
||||
overflow:"hidden",
|
||||
}}>
|
||||
{accent && <div style={{position:"absolute", top:0, left:0, bottom:0, width:3, background:t.fg}}/>}
|
||||
<div style={{fontSize:11, color:"var(--fg-3)", textTransform:"uppercase", letterSpacing:".1em", fontWeight:500}}>{label}</div>
|
||||
<div style={{display:"flex", alignItems:"baseline", gap:6, marginTop:6}}>
|
||||
<span style={{fontFamily:"var(--font-mono)", fontSize:24, fontWeight:600, color:t.fg, fontVariantNumeric:"tabular-nums"}}>{value}</span>
|
||||
{sub && <span style={{fontSize:11, color:"var(--fg-3)"}}>{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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);
|
||||
}, []);
|
||||
|
||||
// 派生重点标记 + 状态等级(用于排序)
|
||||
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;
|
||||
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]);
|
||||
|
||||
const filtered = enriched.filter(v => {
|
||||
if (filterMode === "online" && !(v.gbStatus === "online" || v.jtStatus === "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 === "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;
|
||||
});
|
||||
|
||||
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" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<Sidebar active="integration"/>
|
||||
<div style={{flex:1, display:"flex", flexDirection:"column", minWidth:0, position:"relative", zIndex:1}}>
|
||||
<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 ? "需处理" : "—" },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* 顶部 banner — 如果有完全未对接车辆,全屏告警条 */}
|
||||
{counts.bothNone > 0 && (
|
||||
<div style={{
|
||||
padding:"10px 18px",
|
||||
background:"linear-gradient(90deg, var(--danger-soft), transparent)",
|
||||
borderBottom:"1px solid rgba(179,48,40,0.25)",
|
||||
fontSize:12,
|
||||
display:"flex", alignItems:"center", gap:12, color:"var(--fg-1)",
|
||||
}}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--danger)" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
|
||||
<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>
|
||||
<button className="btn" style={{marginLeft:"auto", height:24, padding:"0 10px", fontSize:11}} onClick={() => setFilterMode("both_none")}>
|
||||
查看全部 →
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 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/>
|
||||
</div>
|
||||
|
||||
{/* 工具栏 */}
|
||||
<div style={{padding:"14px 18px 10px", display:"flex", alignItems:"center", gap:12, flexWrap:"wrap"}}>
|
||||
<div className="search" style={{maxWidth:280, marginLeft:0}}>
|
||||
<Icon name="search" size={13}/>
|
||||
<input placeholder="车牌 / VIN / 品牌 / 型号"
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}/>
|
||||
</div>
|
||||
|
||||
<div style={{display:"flex", gap:6, flexWrap:"wrap"}}>
|
||||
{filters.map(f => (
|
||||
<span key={f.k}
|
||||
onClick={() => 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}
|
||||
<span className="mono" style={{marginLeft:6, fontSize:10, opacity:.75}}>{f.c}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{marginLeft:"auto", display:"flex", gap:8, alignItems:"center", fontSize:11, color:"var(--fg-3)"}}>
|
||||
<span>排序</span>
|
||||
<select value={sortBy} onChange={e => setSortBy(e.target.value)} style={{
|
||||
height:28, padding:"0 8px", fontSize:11, fontFamily:"inherit",
|
||||
background:"var(--bg-1)", border:"1px solid var(--border-1)", borderRadius:6,
|
||||
color:"var(--fg-1)", cursor:"pointer",
|
||||
}}>
|
||||
<option value="priority">优先级(未对接置顶)</option>
|
||||
<option value="plate">车牌升序</option>
|
||||
<option value="gbTime">GB32960 最新</option>
|
||||
<option value="jtTime">JT 最新</option>
|
||||
</select>
|
||||
<button className="btn" style={{height:28, padding:"0 10px", fontSize:11}}>
|
||||
<Icon name="download" size={12}/>
|
||||
导出 CSV
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 表格 */}
|
||||
<div className="scroll" style={{flex:1, padding:"0 18px 18px", minHeight:0, overflow:"auto"}}>
|
||||
<div style={{
|
||||
background:"var(--bg-1)",
|
||||
border:"1px solid var(--border-1)",
|
||||
borderRadius:8, overflow:"hidden",
|
||||
}}>
|
||||
<table className="tbl" style={{width:"100%"}}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{width:24}}></th>
|
||||
<th style={{width:120}}>车牌</th>
|
||||
<th style={{width:200}}>VIN / 车架号</th>
|
||||
<th>品牌 / 型号</th>
|
||||
<th style={{width:130}}>GB32960</th>
|
||||
<th style={{width:170}}>GB32960 最后接收</th>
|
||||
<th style={{width:140}}>JT808 / 1078</th>
|
||||
<th style={{width:170}}>JT 最后接收</th>
|
||||
<th style={{width:120}}>接入时间</th>
|
||||
<th style={{width:80}}></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sorted.map(v => {
|
||||
const flagged = v.isCritical;
|
||||
return (
|
||||
<tr key={v.id} style={{
|
||||
background: flagged ? "rgba(179,48,40,0.05)" : "transparent",
|
||||
cursor:"pointer",
|
||||
}}>
|
||||
<td style={{padding:"10px 6px 10px 14px", verticalAlign:"middle"}}>
|
||||
{flagged ? (
|
||||
<span title="完全未对接 — 重点处理" style={{
|
||||
display:"inline-flex", alignItems:"center", justifyContent:"center",
|
||||
width:20, height:20, borderRadius:10,
|
||||
background:"var(--danger)", color:"#fff",
|
||||
}}>
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>
|
||||
<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"/>
|
||||
</svg>
|
||||
</span>
|
||||
) : v.hasOffline ? (
|
||||
<span title="存在断流通道" style={{display:"inline-block", width:6, height:6, borderRadius:3, background:"var(--warn)", boxShadow:"0 0 4px rgba(181,122,14,0.4)"}}/>
|
||||
) : null}
|
||||
</td>
|
||||
<td className="mono" style={{fontWeight:600, color: flagged ? "var(--danger)" : "var(--fg-0)"}}>
|
||||
{v.plate}
|
||||
</td>
|
||||
<td className="mono" style={{color:"var(--fg-2)", fontSize:11}}>
|
||||
{v.vin}
|
||||
</td>
|
||||
<td>
|
||||
<div style={{display:"flex", flexDirection:"column", gap:2}}>
|
||||
<span style={{fontWeight:500, color:"var(--fg-0)", fontSize:12}}>{v.brand}</span>
|
||||
<span style={{color:"var(--fg-3)", fontSize:11}}>{v.model}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<ConnPill status={v.gbStatus} protocol="GB"/>
|
||||
</td>
|
||||
<td className="mono" style={{color: v.gbStatus === "not_connected" ? "var(--fg-3)" : v.gbStatus === "offline" ? "var(--warn)" : "var(--fg-1)"}}>
|
||||
{v.gbLastSeen ? (
|
||||
<div style={{display:"flex", flexDirection:"column", gap:1, fontSize:11}}>
|
||||
<span>{fmtRelative(v.gbLastSeen)}</span>
|
||||
<span style={{color:"var(--fg-3)", fontSize:10}}>{fmtAbsolute(v.gbLastSeen)}</span>
|
||||
</div>
|
||||
) : <span style={{color:"var(--fg-3)"}}>—</span>}
|
||||
</td>
|
||||
<td>
|
||||
<ConnPill status={v.jtStatus} protocol="JT"/>
|
||||
</td>
|
||||
<td className="mono" style={{color: v.jtStatus === "not_connected" ? "var(--fg-3)" : v.jtStatus === "offline" ? "var(--warn)" : "var(--fg-1)"}}>
|
||||
{v.jtLastSeen ? (
|
||||
<div style={{display:"flex", flexDirection:"column", gap:1, fontSize:11}}>
|
||||
<span>{fmtRelative(v.jtLastSeen)}</span>
|
||||
<span style={{color:"var(--fg-3)", fontSize:10}}>{fmtAbsolute(v.jtLastSeen)}</span>
|
||||
</div>
|
||||
) : <span style={{color:"var(--fg-3)"}}>—</span>}
|
||||
</td>
|
||||
<td className="mono" style={{color:"var(--fg-2)", fontSize:11}}>
|
||||
{fmtDate(v.onboardAt)}
|
||||
</td>
|
||||
<td>
|
||||
<button className="btn" style={{height:24, padding:"0 8px", fontSize:11}} onClick={() => location.hash = "#/detail"}>
|
||||
查看
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{sorted.length === 0 && (
|
||||
<tr><td colSpan={10} style={{textAlign:"center", padding:40, color:"var(--fg-3)", fontSize:12}}>没有匹配的车辆</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style={{padding:"10px 4px", display:"flex", justifyContent:"space-between", fontSize:11, color:"var(--fg-3)"}}>
|
||||
<span>共 <span className="mono strong" style={{color:"var(--fg-1)"}}>{sorted.length}</span> 辆车 · 数据每 30 秒刷新一次</span>
|
||||
<span>处理流程:发现未对接 → 在车联网平台下工单 → 配置 TBOX/JT 主机参数 → 回流验证</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
window.ArtboardIntegration = ArtboardIntegration;
|
||||
@@ -1,12 +1,13 @@
|
||||
// chrome.jsx — Sidebar, Topbar shared chrome (route-aware)
|
||||
|
||||
const SIDEBAR_ITEMS = [
|
||||
{ id: "overview", icon: "map", label: "实时地图" },
|
||||
{ id: "detail", icon: "car", label: "车辆详情" },
|
||||
{ id: "history", icon: "history", label: "历史查询" },
|
||||
{ id: "playback", icon: "route", label: "轨迹回放" },
|
||||
{ id: "alarm", icon: "bell", label: "事件规则" },
|
||||
{ id: "inbox", icon: "inbox", label: "通知中心" },
|
||||
{ id: "overview", icon: "map", label: "实时地图" },
|
||||
{ id: "detail", icon: "car", label: "车辆详情" },
|
||||
{ id: "history", icon: "history", label: "历史查询" },
|
||||
{ id: "playback", icon: "route", label: "轨迹回放" },
|
||||
{ id: "alarm", icon: "bell", label: "事件规则" },
|
||||
{ id: "inbox", icon: "inbox", label: "通知中心" },
|
||||
{ id: "integration", icon: "plug", label: "数据接入监控" },
|
||||
];
|
||||
const SIDEBAR_SUB = [
|
||||
{ id: "esg", icon: "chart", label: "ESG·碳减排" },
|
||||
|
||||
@@ -133,6 +133,60 @@ const _enrich = (v, i) => {
|
||||
const h2 = v.status === "danger" ? 0.8 : (v.soc / 100 * 5.6 + 0.2).toFixed(1);
|
||||
const motorTemp = v.status === "danger" ? 102 : 58 + Math.floor(r()*15);
|
||||
|
||||
// ── Brand & model — 真实国内氢能车型样本 ──
|
||||
const MODELS = [
|
||||
{ brand: "上汽大通", model: "MIFA-H 氢燃料 MPV" },
|
||||
{ brand: "上汽大通", model: "EUNIQ 7 氢电版" },
|
||||
{ brand: "现代", model: "NEXO" },
|
||||
{ brand: "丰田", model: "MIRAI 第二代" },
|
||||
{ brand: "格罗夫", model: "格罗夫氢能 SUV" },
|
||||
{ brand: "海马汽车", model: "7X-H" },
|
||||
{ brand: "红旗", model: "H5 FCV" },
|
||||
{ brand: "长安深蓝", model: "SL03 氢燃料版" },
|
||||
{ brand: "飞驰科技", model: "FCB80 氢燃料客车" },
|
||||
{ brand: "宇通客车", model: "ZK6105FCEVG3" },
|
||||
];
|
||||
const m = MODELS[Math.floor(r() * MODELS.length)];
|
||||
|
||||
// ── Integration / 对接情况 ──
|
||||
// src: T = TBOX(GB/T 32960), J = JT808/1078, B = both
|
||||
// 状态:online (recent) / offline (had data, now stale) / not_connected (从未对接)
|
||||
// 5% 完全未对接(重点标记) · 12% TBOX 断流 · 8% JT 断流
|
||||
const integFlag = r();
|
||||
let gbStatus, jtStatus;
|
||||
if (i >= 12 && integFlag < 0.05) {
|
||||
// 5% 全部未对接(重点标记)
|
||||
gbStatus = "not_connected";
|
||||
jtStatus = "not_connected";
|
||||
} else {
|
||||
if (v.src === "T" || v.src === "B") {
|
||||
gbStatus = (r() < 0.12) ? "offline" : "online";
|
||||
} else {
|
||||
gbStatus = (r() < 0.30) ? "offline" : "not_connected";
|
||||
}
|
||||
if (v.src === "J" || v.src === "B") {
|
||||
jtStatus = (r() < 0.08) ? "offline" : "online";
|
||||
} else {
|
||||
jtStatus = (r() < 0.20) ? "offline" : "not_connected";
|
||||
}
|
||||
}
|
||||
|
||||
// 时间戳生成
|
||||
const now = Date.now();
|
||||
const minute = 60 * 1000, hour = 60 * minute, day = 24 * hour;
|
||||
const tsFor = (st, baseSeed) => {
|
||||
if (st === "not_connected") return null;
|
||||
if (st === "online") return now - Math.floor(baseSeed * 5 * minute) - 2000; // 2s ~ 5min
|
||||
// offline
|
||||
return now - Math.floor(baseSeed * 7 * day) - 30 * minute; // 30min ~ 7d ago
|
||||
};
|
||||
const gbLastSeen = tsFor(gbStatus, r());
|
||||
const jtLastSeen = tsFor(jtStatus, r());
|
||||
|
||||
// 接入时间(首次对接日期,6 个月内随机)
|
||||
const onboardDays = Math.floor(r() * 180) + 7;
|
||||
const onboardAt = now - onboardDays * day;
|
||||
|
||||
return {
|
||||
...v,
|
||||
plate: v.id,
|
||||
@@ -148,6 +202,12 @@ const _enrich = (v, i) => {
|
||||
// Hydrogen-specific
|
||||
h2Pressure: parseFloat(h2),
|
||||
range: Math.round(v.soc * 6.2),
|
||||
// Brand / model
|
||||
brand: m.brand, model: m.model,
|
||||
// Integration / 对接情况
|
||||
gbStatus, gbLastSeen,
|
||||
jtStatus, jtLastSeen,
|
||||
onboardAt,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -175,6 +235,17 @@ const COUNTS = {
|
||||
acc[d.id] = VEHICLES.filter(v => v.dept === d.id).length;
|
||||
return acc;
|
||||
}, {}),
|
||||
// Integration / 数据接入
|
||||
gbOnline: VEHICLES.filter(v => v.gbStatus === "online").length,
|
||||
gbOffline: VEHICLES.filter(v => v.gbStatus === "offline").length,
|
||||
gbNotConn: VEHICLES.filter(v => v.gbStatus === "not_connected").length,
|
||||
jtOnline: VEHICLES.filter(v => v.jtStatus === "online").length,
|
||||
jtOffline: VEHICLES.filter(v => v.jtStatus === "offline").length,
|
||||
jtNotConn: VEHICLES.filter(v => v.jtStatus === "not_connected").length,
|
||||
// 完全未对接(重点标记)
|
||||
bothNone: VEHICLES.filter(v => v.gbStatus === "not_connected" && v.jtStatus === "not_connected").length,
|
||||
// 任一在线
|
||||
anyOnline: VEHICLES.filter(v => v.gbStatus === "online" || v.jtStatus === "online").length,
|
||||
};
|
||||
|
||||
// ── User roles for permission demo ────────────────────────
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
<script type="text/babel" src="artboards/playback.jsx"></script>
|
||||
<script type="text/babel" src="artboards/alarm.jsx"></script>
|
||||
<script type="text/babel" src="artboards/inbox.jsx"></script>
|
||||
<script type="text/babel" src="artboards/integration.jsx"></script>
|
||||
<script type="text/babel" src="artboards/esg.jsx"></script>
|
||||
<script type="text/babel" src="artboards/variant-light.jsx"></script>
|
||||
<script type="text/babel" src="artboards/variant-dense.jsx"></script>
|
||||
|
||||
Reference in New Issue
Block a user