// artboard-history.jsx — 数据检索 (data search studio) // Flow: ① 选车辆+时段 → ② 选数据项目 → ③ 选展示方式 → ④ 渲染结果 const DATA_GROUPS = [ { id: "vehicle", label: "车辆运行", icon: "car", color: "var(--info)", items: [ {id:"speed", l:"速度", u:"km/h", src:"TBOX", freq:"10s"}, {id:"odometer", l:"累计里程", u:"km", src:"TBOX", freq:"60s"}, {id:"trip_km", l:"行程里程", u:"km", src:"TBOX", freq:"事件"}, {id:"engine_run", l:"运行时长", u:"s", src:"TBOX", freq:"60s"}, {id:"gear", l:"档位", u:"-", src:"TBOX", freq:"10s"}, {id:"steer", l:"方向盘转角", u:"°", src:"CAN", freq:"100ms"}, ], }, { id: "energy", label: "氢电系统", icon: "h2", color: "var(--accent)", items: [ {id:"soc", l:"动力电池 SOC", u:"%", src:"BMS", freq:"10s"}, {id:"batt_volt", l:"电池总压", u:"V", src:"BMS", freq:"10s"}, {id:"batt_curr", l:"电池电流", u:"A", src:"BMS", freq:"10s"}, {id:"batt_temp", l:"电池温度", u:"℃", src:"BMS", freq:"10s"}, {id:"h2_pressure",l:"H₂ 压力", u:"MPa", src:"FCU", freq:"10s"}, {id:"h2_flow", l:"H₂ 流量", u:"g/s", src:"FCU", freq:"10s"}, {id:"fc_power", l:"电堆输出功率", u:"kW", src:"FCU", freq:"10s"}, {id:"fc_temp", l:"电堆温度", u:"℃", src:"FCU", freq:"10s"}, ], }, { id: "chassis", label: "底盘 & 安全", icon: "shield", color: "var(--warn)", items: [ {id:"tire_p_fl", l:"胎压 左前", u:"MPa", src:"TPMS", freq:"60s"}, {id:"tire_p_fr", l:"胎压 右前", u:"MPa", src:"TPMS", freq:"60s"}, {id:"tire_p_rl", l:"胎压 左后", u:"MPa", src:"TPMS", freq:"60s"}, {id:"tire_p_rr", l:"胎压 右后", u:"MPa", src:"TPMS", freq:"60s"}, {id:"brake", l:"制动信号", u:"0/1", src:"CAN", freq:"100ms"}, {id:"airbag", l:"安全气囊状态", u:"0/1", src:"CAN", freq:"事件"}, {id:"abs", l:"ABS 状态", u:"0/1", src:"CAN", freq:"事件"}, ], }, { id: "location", label: "定位 & 通讯", icon: "pin", color: "var(--info)", items: [ {id:"gps_lat", l:"GPS 纬度", u:"°", src:"JT808", freq:"10s"}, {id:"gps_lng", l:"GPS 经度", u:"°", src:"JT808", freq:"10s"}, {id:"gps_alt", l:"海拔", u:"m", src:"JT808", freq:"10s"}, {id:"signal", l:"信号强度", u:"dBm", src:"TBOX", freq:"60s"}, {id:"network", l:"网络类型", u:"-", src:"TBOX", freq:"60s"}, ], }, { id: "driving", label: "驾驶行为", icon: "speed", color: "var(--accent)", items: [ {id:"hard_acc", l:"急加速次数", u:"次/h", src:"TBOX", freq:"事件"}, {id:"hard_brake", l:"急刹车次数", u:"次/h", src:"TBOX", freq:"事件"}, {id:"sharp_turn", l:"急转弯次数", u:"次/h", src:"TBOX", freq:"事件"}, {id:"overspeed", l:"超速时长", u:"s/h", src:"TBOX", freq:"事件"}, {id:"score", l:"驾驶评分", u:"分", src:"算法", freq:"日"}, ], }, ]; const VIEW_MODES = [ {id:"line", l:"曲线图", ic:"chart", d:"时间序列趋势"}, {id:"area", l:"面积图", ic:"pulse", d:"叠加趋势对比"}, {id:"bar", l:"柱状图", ic:"layers", d:"分时统计"}, {id:"table", l:"数据表", ic:"list", d:"按时间戳列表"}, {id:"heat", l:"热力日历", ic:"history", d:"按日聚合"}, {id:"summary",l:"统计摘要", ic:"gauge", d:"min/max/avg/p95"}, ]; const QUICK_RANGES = [ {id:"1h", l:"近1小时"}, {id:"6h", l:"近6小时"}, {id:"24h", l:"近24小时"}, {id:"7d", l:"近7日"}, {id:"30d", l:"近30日"}, {id:"custom", l:"自定义"}, ]; const ArtboardHistory = () => { // Wizard step: 1 选范围, 2 选项目, 3 展示 const [step, setStep] = React.useState(3); const [vehicle] = React.useState({plate:"浙F03980F", vin:"LJ2A...8814", dept:"业务一部"}); const [range, setRange] = React.useState("24h"); const [dateFrom] = React.useState("2026-04-27 14:02"); const [dateTo] = React.useState("2026-04-28 14:02"); // Selected data items (ids) const [picked, setPicked] = React.useState(new Set(["speed","soc","h2_pressure"])); const [activeGroup, setActiveGroup] = React.useState("vehicle"); // Visualization mode const [view, setView] = React.useState("line"); const togglePick = (id) => { const next = new Set(picked); if (next.has(id)) next.delete(id); else next.add(id); setPicked(next); }; const pickedItems = DATA_GROUPS.flatMap(g => g.items.filter(it => picked.has(it.id)).map(it => ({...it, group:g}))); return (
{/* Step indicator */}
{[ {n:1, l:"选择范围"}, {n:2, l:"选择数据项目"}, {n:3, l:"选择展示方式"}, ].map((s,i,arr)=>(
= s.n ? 1 : 0.5}} onClick={() => setStep(s.n)}> = s.n ? "var(--accent)" : "var(--bg-3)", color: step >= s.n ? "#fff" : "var(--fg-3)", display:"grid", placeItems:"center", fontSize:11, fontWeight:600, fontFamily:"var(--font-mono)", }}>{s.n} {s.l}
{i < arr.length - 1 && }
))}
已选 {picked.size}
{/* Main */}
{/* LEFT: range + data items selector */}
{/* Vehicle + time */}
查询对象
{vehicle.plate}
{vehicle.vin} · {vehicle.dept}
时间范围
{QUICK_RANGES.map(r => ( setRange(r.id)} className={"chip " + (range === r.id ? "accent" : "")} style={{justifyContent:"center", cursor:"pointer", fontSize:10, padding:"4px 0"}}> {r.l} ))}
起 → 止
{dateFrom}
{dateTo}
{/* Data item picker */}
数据项目 已选 {picked.size}
{DATA_GROUPS.map(g => ( setActiveGroup(g.id)} className={"chip " + (activeGroup === g.id ? "accent" : "")} style={{cursor:"pointer", fontSize:10, padding:"3px 8px"}}> {g.label} {g.items.filter(it => picked.has(it.id)).length || g.items.length} ))}
{DATA_GROUPS.filter(g => g.id === activeGroup).map(g => (
{g.items.map(it => { const on = picked.has(it.id); return (
togglePick(it.id)} style={{ display:"flex", alignItems:"center", gap:10, padding:"9px 14px", borderBottom:"1px solid var(--border-1)", cursor:"pointer", background: on ? "var(--accent-soft)" : "transparent", }}> {on && }
{it.l} {it.u}
{it.src} · {it.freq}
); })}
))}
{/* RIGHT: visualization */}
{/* View-mode picker */}
展示 {VIEW_MODES.map(v => ( setView(v.id)} style={{ display:"flex", alignItems:"center", gap:6, padding:"5px 10px", borderRadius:5, cursor:"pointer", fontSize:11, background: view === v.id ? "var(--accent-soft)" : "transparent", border: "1px solid " + (view === v.id ? "var(--accent)" : "var(--border-1)"), color: view === v.id ? "var(--accent)" : "var(--fg-1)", }}> {v.l} ))}
采样: 原始 1分钟 5分钟 1小时
{/* Selected items pills */}
{pickedItems.length === 0 && 请从左侧选择数据项} {pickedItems.map(it => ( {it.l} {it.u} togglePick(it.id)}>× ))}
{/* Render area */}
{view === "line" && } {view === "area" && } {view === "bar" && } {view === "table" && } {view === "heat" && } {view === "summary" && }
); }; // ── Visualizations ─────────────────────────────────────────── const colorFor = (it, fallback) => { if (it.id === "speed" || it.id.startsWith("hard_") || it.id === "h2_flow") return "var(--info)"; if (it.id === "soc" || it.id.startsWith("batt") || it.id === "fc_power") return "var(--accent)"; if (it.id.startsWith("h2_") || it.id.startsWith("fc_temp")) return "var(--warn)"; if (it.id.startsWith("tire_")) return "var(--danger)"; return it.group?.color || fallback || "var(--accent)"; }; const synth = (id, n=120) => { // Deterministic per-id pseudo-random series const seed = id.split("").reduce((s,c) => s + c.charCodeAt(0), 0); const out = []; for (let i=0;i { if (!items.length) return ; return (
{items.map(it => (
{it.l} {it.u}
{it.src} · {it.freq} CSV
))}
); }; const AreaView = ({ items }) => { if (!items.length) return ; return (
{items.length} 项叠加
{items.map(it => ( {it.l} ))}
{[0,0.25,0.5,0.75,1].map((p,i) => ( ))} {items.map((it,idx) => { const data = synth(it.id, 240); const path = data.map((v,i) => `${(i/(data.length-1))*800},${310 - v*300}`).join(" L"); return ( ); })}
); }; const BarView = ({ items }) => { if (!items.length) return ; const buckets = ["00","02","04","06","08","10","12","14","16","18","20","22"]; return (
{items.map(it => { const data = synth(it.id, buckets.length).map(v => v * 100); return (
{it.l} · 按 2 小时聚合
{it.u}
{data.map((v,i) => { const x = i * (720/data.length) + 8; const w = (720/data.length) - 16; const h = (v/100) * 100; return ( {buckets[i]} ); })}
); })}
); }; const TableView = ({ items }) => { if (!items.length) return ; // 30 rows of timestamps const rows = Array.from({length:40}, (_,i) => { const d = new Date(2026, 3, 28, 14, 0, 0); d.setMinutes(d.getMinutes() - i * 5); const ts = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")} ${String(d.getHours()).padStart(2,"0")}:${String(d.getMinutes()).padStart(2,"0")}:00`; const vals = items.map(it => synth(it.id, 240)[i % 240]); return { ts, vals }; }); return (
数据列表 共 1,728 行 · 显示 40
筛选 CSV
{items.map(it => ( ))} {rows.map((r,i) => ( {r.vals.map((v,j) => { const it = items[j]; let display; if (it.id === "speed") display = (v*80).toFixed(1); else if (it.id === "soc") display = (20 + v*70).toFixed(1); else if (it.id.startsWith("h2_pressure")) display = (3.5 + v*1.3).toFixed(2); else if (it.id.startsWith("tire_")) display = (2.7 + v*0.6).toFixed(2); else if (it.id.startsWith("batt_temp")) display = (24 + v*22).toFixed(1); else if (it.id === "odometer") display = (124820 + v*4).toFixed(1); else display = (v*100).toFixed(2); return ; })} ))}
时间戳
{it.l}
{it.u} · {it.src}
{r.ts}{display}
); }; const HeatView = ({ items }) => { if (!items.length) return ; return (
{items.map(it => { const cells = synth(it.id, 30); // 30 days return (
{it.l} · 30 日热力
{it.u}
{cells.map((v,i) => (
))}
30 日前今日
); })}
); }; const SummaryView = ({ items }) => { if (!items.length) return ; return (
{items.map(it => { const data = synth(it.id, 240).map(v => v*100); const min = Math.min(...data), max = Math.max(...data); const avg = data.reduce((s,v)=>s+v,0)/data.length; const sorted = [...data].sort((a,b)=>a-b); const p95 = sorted[Math.floor(sorted.length*0.95)]; const p50 = sorted[Math.floor(sorted.length*0.50)]; // Scale to plausible units const fmt = (v) => { if (it.id === "speed") return (v*0.8).toFixed(1); if (it.id === "soc") return (20 + v*0.7).toFixed(1); if (it.id.startsWith("h2_pressure")) return (3.5 + v*0.013).toFixed(2); return v.toFixed(1); }; return (
{it.l}
{it.u} · {it.src}
{[ {l:"平均", v:fmt(avg), c:colorFor(it)}, {l:"中位", v:fmt(p50), c:"var(--fg-1)"}, {l:"最大", v:fmt(max), c:"var(--danger)"}, {l:"最小", v:fmt(min), c:"var(--ok)"}, {l:"P95", v:fmt(p95), c:"var(--warn)"}, {l:"样本", v:"1,728", c:"var(--fg-2)"}, ].map((s,i) => (
{s.l}
{s.v}
))}
); })}
); }; // Sparkline + axis helpers const Sparkline = ({ data, h=80, color="var(--accent)", fill=false, axis=false }) => { const w = 800; const max = Math.max(...data, 0.01); const path = data.map((v,i) => `${(i/(data.length-1))*w},${h - (v/max) * (h-12) - 6}`).join(" L"); return ( {axis && [0,0.25,0.5,0.75,1].map((p,i) => ( ))} {fill && } ); }; const TimeAxis = () => (
{["00:00","04:00","08:00","12:00","16:00","20:00","24:00"].map(t => {t})}
); const EmptyHint = () => (
从左侧选择数据项目以开始检索
支持速度 / SOC / H₂ 压力 / 胎压 / 驾驶行为等 30+ 字段
); window.ArtboardHistory = ArtboardHistory;