// 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}
))}
);
};
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 (
);
})}
);
};
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 => (
{it.l}
{it.u} · {it.src}
|
))}
{rows.map((r,i) => (
| {r.ts} |
{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 {display} | ;
})}
))}
);
};
const HeatView = ({ items }) => {
if (!items.length) return ;
return (
{items.map(it => {
const cells = synth(it.id, 30); // 30 days
return (
{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 (
{[
{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) => (
))}
);
})}
);
};
// 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 (
);
};
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;