From e38bd8a1d81156bead58f1b8ba0e7a7666566d8b Mon Sep 17 00:00:00 2001 From: kkfluous Date: Tue, 28 Apr 2026 15:45:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(integration):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=8E=A5=E5=85=A5=E7=9B=91=E6=8E=A7=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 字段:车牌 / 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 图标项 --- app.jsx | 5 +- artboards/integration.jsx | 350 ++++++++++++++++++++++++++++++++++++++ components/chrome.jsx | 13 +- data/fleet.js | 71 ++++++++ 羚牛车辆数据中心.html | 1 + 5 files changed, 432 insertions(+), 8 deletions(-) create mode 100644 artboards/integration.jsx diff --git a/app.jsx b/app.jsx index 0089db8..aedae2e 100644 --- a/app.jsx +++ b/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 = [ diff --git a/artboards/integration.jsx b/artboards/integration.jsx new file mode 100644 index 0000000..034968c --- /dev/null +++ b/artboards/integration.jsx @@ -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 ( + + + {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); + }, []); + + // 派生重点标记 + 状态等级(用于排序) + 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 ( +
+ +
+ 0 ? "需处理" : "—" }, + ]} + /> + + {/* 顶部 banner — 如果有完全未对接车辆,全屏告警条 */} + {counts.bothNone > 0 && ( +
+ + + + + 检测到 {counts.bothNone} 辆车 GB32960 与 JT808 双协议均未对接,可能为新增/更换车机后未配置上行 — 请尽快在对应业务系统下工单核查。 + +
+ )} + + {/* 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 / 车架号品牌 / 型号GB32960GB32960 最后接收JT808 / 1078JT 最后接收接入时间
+ {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)} +
+ ) : } +
+ {fmtDate(v.onboardAt)} + + +
没有匹配的车辆
+
+ +
+ {sorted.length} 辆车 · 数据每 30 秒刷新一次 + 处理流程:发现未对接 → 在车联网平台下工单 → 配置 TBOX/JT 主机参数 → 回流验证 +
+
+
+
+ ); +}; + +window.ArtboardIntegration = ArtboardIntegration; diff --git a/components/chrome.jsx b/components/chrome.jsx index d7dcfb1..181c71c 100644 --- a/components/chrome.jsx +++ b/components/chrome.jsx @@ -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·碳减排" }, diff --git a/data/fleet.js b/data/fleet.js index e9673aa..a4555c3 100644 --- a/data/fleet.js +++ b/data/fleet.js @@ -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 ──────────────────────── diff --git a/羚牛车辆数据中心.html b/羚牛车辆数据中心.html index 0c7971c..d224830 100644 --- a/羚牛车辆数据中心.html +++ b/羚牛车辆数据中心.html @@ -52,6 +52,7 @@ +