feat(integration): 接入 1006 辆真实车辆数据 + 24h 异常派生 + 重点品牌锚定
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
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:
@@ -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>
|
||||
|
||||
@@ -15,6 +15,8 @@ const DEPARTMENTS = [
|
||||
{ id: "biz2", name: "业务二部", lead: "陈高伟", color: "#2E8C8C" },
|
||||
{ id: "biz3", name: "业务三部", lead: "尚建华", color: "#7A8C2E" },
|
||||
{ id: "biz4", name: "业务四部", lead: "刘念念", color: "#C97A3D" },
|
||||
{ id: "biz5", name: "业务五部", lead: "周志强", color: "#7A4FB4" },
|
||||
{ id: "biz6", name: "业务六部", lead: "金可鹏", color: "#B45F8A" },
|
||||
{ id: "ops", name: "运营部", lead: "张兰", color: "#5C6E7C" },
|
||||
];
|
||||
|
||||
@@ -211,7 +213,61 @@ const _enrich = (v, i) => {
|
||||
};
|
||||
};
|
||||
|
||||
const VEHICLES = [..._mapped, ..._extra].map(_enrich);
|
||||
// ── Build VEHICLES ────────────────────────────────────────
|
||||
// 优先使用 vehicles-real.js 提供的 1006 辆真实车辆数据;不存在则回退到合成数据。
|
||||
let VEHICLES;
|
||||
if (window.RAW_VEHICLES && window.RAW_VEHICLES.length) {
|
||||
// 从 xlsx 抽取的 1006 辆真实数据
|
||||
// 前 12 辆叠加乍浦港地图坐标,让总览地图保持原本的演示态
|
||||
const RAW = window.RAW_VEHICLES;
|
||||
const MAP_OVERLAY = [
|
||||
{ x: 320, y: 180, h: 320, status: "ok", speed: 56, soc: 78 },
|
||||
{ x: 460, y: 280, h: 60, status: "warn", speed: 0, soc: 24 },
|
||||
{ x: 600, y: 240, h: 110, status: "ok", speed: 78, soc: 31 },
|
||||
{ x: 760, y: 290, h: 200, status: "ok", speed: 64, soc: 82 },
|
||||
{ x: 880, y: 410, h: 280, status: "ok", speed: 51, soc: 47 },
|
||||
{ x: 240, y: 540, h: 30, status: "idle", speed: 0, soc: 96 },
|
||||
{ x: 600, y: 470, h: 160, status: "ok", speed: 44, soc: 55 },
|
||||
{ x: 540, y: 380, h: 240, status: "ok", speed: 38, soc: 71 },
|
||||
{ x: 1000,y: 360, h: 90, status: "danger", speed: 0, soc: 9 },
|
||||
{ x: 700, y: 540, h: 350, status: "ok", speed: 42, soc: 64 },
|
||||
{ x: 960, y: 540, h: 70, status: "warn", speed: 0, soc: 18 },
|
||||
{ x: 820, y: 470, h: 190, status: "ok", speed: 48, soc: 88 },
|
||||
];
|
||||
|
||||
VEHICLES = RAW.map((v, i) => {
|
||||
const overlay = i < MAP_OVERLAY.length ? MAP_OVERLAY[i] : { x:null, y:null, h:0, status:"ok", speed:0, soc:0 };
|
||||
const dept = DEPARTMENTS.find(d => d.id === v.dept) || DEPARTMENTS[DEPARTMENTS.length-1];
|
||||
// 派生氢电指标(xlsx 没有这些字段)
|
||||
const soc = overlay.soc || ((i * 17) % 90 + 10);
|
||||
const h2 = overlay.status === "danger" ? 0.8 : +(soc / 100 * 5.6 + 0.2).toFixed(1);
|
||||
const motorTemp = overlay.status === "danger" ? 102 : 58 + (i % 15);
|
||||
const totalKm = 12000 + ((i * 1331) % 80000);
|
||||
const lastMaintKm = totalKm - 1000 - ((i * 379) % 8000);
|
||||
const nextMaintKm = lastMaintKm + 10000;
|
||||
const kmToMaint = nextMaintKm - totalKm;
|
||||
const lastMaintDays = 1 + ((i * 41) % 90);
|
||||
return {
|
||||
...v,
|
||||
// overlay map fields
|
||||
x: overlay.x, y: overlay.y, h: overlay.h,
|
||||
status: overlay.status, // for map color (ok/warn/danger/idle)
|
||||
speed: overlay.speed, soc,
|
||||
// department display fields
|
||||
deptName: dept.name, deptLead: dept.lead, deptColor: dept.color,
|
||||
// hydrogen-electric derived
|
||||
h2, h2Pressure: parseFloat(h2),
|
||||
range: Math.round(soc * 6.2),
|
||||
motorTemp,
|
||||
totalKm, lastMaintKm, nextMaintKm, kmToMaint, lastMaintDays,
|
||||
// legacy/compat
|
||||
fleetCode: null,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// Fallback: 合成数据
|
||||
VEHICLES = [..._mapped, ..._extra].map(_enrich);
|
||||
}
|
||||
|
||||
// ── Aggregations for filters ──────────────────────────────
|
||||
const COUNTS = {
|
||||
|
||||
4
data/vehicles-real.js
Normal file
4
data/vehicles-real.js
Normal file
File diff suppressed because one or more lines are too long
@@ -2,7 +2,7 @@
|
||||
|
||||
## 一句话定位
|
||||
|
||||
**面向氢能乘用车队的物联网 + AI + 大数据驾驶舱** —— 把车端国标遥信、北斗位置、车载视频和业务系统数据汇聚为统一资产视图,从「能看车」延伸到「会算账、能预警、可决策」的一体化数据中台。
|
||||
**面向氢能车运营平台的物联网 + AI + 大数据驾驶舱** —— 把车端国标遥信、北斗位置、车载视频和业务系统数据汇聚为统一资产视图,从「能看车」延伸到「会算账、能预警、可决策」的一体化数据中台。
|
||||
|
||||
---
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
|
||||
### ④ AI 智能层 · 让数据自己说话
|
||||
|
||||
- **异常检测** — 基于多维时序的 Isolation Forest / LSTM-AD,对电池一致性、电机温升、氢气泄漏给出**早于阈值告警**的预测
|
||||
- **异常检测** — 基于多维时序的 Isolation Forest (孤立森林)/ LSTM-AD(时序行为异常检测),对电池一致性、电机温升、氢气泄漏给出**早于阈值告警**的预测
|
||||
- **驾驶行为评分** — 急加速 / 急减速 / 急转弯 / 超速时长加权,输出 A–E 评分卡
|
||||
- **能耗 & 续航预测** — XGBoost 结合载重、风速、坡度、温度,对剩余里程做误差 < 5% 的滚动预测
|
||||
- **轨迹挖掘** — DBSCAN 识别停留点、聚类常用路线,反哺补能站选址与电子围栏自动生成
|
||||
@@ -38,6 +38,7 @@
|
||||
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
|
||||
<script src="data/vehicles-real.js"></script>
|
||||
<script src="data/fleet.js"></script>
|
||||
<script type="text/babel" src="design-canvas.jsx"></script>
|
||||
<script type="text/babel" src="tweaks-panel.jsx"></script>
|
||||
|
||||
Reference in New Issue
Block a user