All checks were successful
ci/woodpecker/manual/woodpecker Pipeline was successful
- React 18 + Babel-in-browser SPA 原型,覆盖 8 个画板: 实时地图 / 车辆详情 / 历史查询 / 轨迹回放 / 事件规则 / 通知中心 / ESG 碳减排 / 移动端 - 设计系统:IBM Plex Sans + JetBrains Mono,亮/暗双主题,羚牛绿 #007143 - 数据模型:12 + 40 辆车,TBOX (T) / JT808+1078 (JT) / 双源 (B) - 部署:nginx 静态托管,Dockerfile + woodpecker.yml + docker-compose.yml - 镜像:harbor.lnh2e.com/lingniu-v1/ln-vdc:<branch>-<VERSION> - 容器端口 80,宿主映射 8112,含 /healthz 探活
151 lines
5.5 KiB
JavaScript
151 lines
5.5 KiB
JavaScript
// charts.jsx — minimal SVG charts for the cockpit
|
|
|
|
// Sparkline / line chart
|
|
const LineChart = ({ data, w = 300, h = 80, color = "var(--accent)", fill = true, axis = false, showDots = false, baseline = null }) => {
|
|
const min = Math.min(...data), max = Math.max(...data);
|
|
const span = max - min || 1;
|
|
const stepX = w / (data.length - 1);
|
|
const padY = 6;
|
|
const y = v => padY + (h - padY * 2) * (1 - (v - min) / span);
|
|
const pts = data.map((v, i) => [i * stepX, y(v)]);
|
|
const path = pts.map((p, i) => (i === 0 ? "M" : "L") + p[0] + " " + p[1]).join(" ");
|
|
const areaPath = path + ` L ${w} ${h} L 0 ${h} Z`;
|
|
|
|
return (
|
|
<svg width={w} height={h} viewBox={`0 0 ${w} ${h}`} style={{ display: "block" }}>
|
|
<defs>
|
|
<linearGradient id={`g-${color.replace(/[^a-z]/gi,'')}`} x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="0" stopColor={color} stopOpacity="0.35"/>
|
|
<stop offset="1" stopColor={color} stopOpacity="0"/>
|
|
</linearGradient>
|
|
</defs>
|
|
{axis && (
|
|
<>
|
|
<line x1="0" y1={h-1} x2={w} y2={h-1} stroke="var(--border-1)" strokeWidth="1"/>
|
|
{[0.25, 0.5, 0.75].map(p => (
|
|
<line key={p} x1="0" y1={h*p} x2={w} y2={h*p} stroke="var(--border-1)" strokeWidth="0.5" strokeDasharray="2 3"/>
|
|
))}
|
|
</>
|
|
)}
|
|
{baseline != null && (
|
|
<line x1="0" y1={y(baseline)} x2={w} y2={y(baseline)} stroke="var(--warn)" strokeWidth="1" strokeDasharray="3 3" opacity="0.6"/>
|
|
)}
|
|
{fill && <path d={areaPath} fill={`url(#g-${color.replace(/[^a-z]/gi,'')})`}/>}
|
|
<path d={path} fill="none" stroke={color} strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="round"/>
|
|
{showDots && pts.map((p, i) => (
|
|
<circle key={i} cx={p[0]} cy={p[1]} r="2" fill={color}/>
|
|
))}
|
|
</svg>
|
|
);
|
|
};
|
|
|
|
const Bars = ({ data, w = 300, h = 80, color = "var(--accent)", labels = null }) => {
|
|
const max = Math.max(...data) || 1;
|
|
const slot = w / data.length;
|
|
const bw = Math.max(slot * 0.55, 3);
|
|
return (
|
|
<svg width={w} height={h} viewBox={`0 0 ${w} ${h}`} style={{ display: "block" }}>
|
|
{data.map((v, i) => {
|
|
const bh = (h - 14) * (v / max);
|
|
return (
|
|
<g key={i}>
|
|
<rect x={i*slot + (slot-bw)/2} y={h - 14 - bh} width={bw} height={bh} fill={color} rx="1.5" opacity={0.85}/>
|
|
{labels && <text x={i*slot + slot/2} y={h-3} textAnchor="middle" fill="var(--fg-3)" fontSize="9" fontFamily="var(--font-mono)">{labels[i]}</text>}
|
|
</g>
|
|
);
|
|
})}
|
|
</svg>
|
|
);
|
|
};
|
|
|
|
// Donut for pie share
|
|
const Donut = ({ size = 80, value = 0.7, color = "var(--accent)", track = "var(--bg-3)", thick = 8, label }) => {
|
|
const r = size/2 - thick/2;
|
|
const c = 2*Math.PI*r;
|
|
return (
|
|
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
|
|
<circle cx={size/2} cy={size/2} r={r} fill="none" stroke={track} strokeWidth={thick}/>
|
|
<circle cx={size/2} cy={size/2} r={r} fill="none" stroke={color} strokeWidth={thick}
|
|
strokeDasharray={`${c*value} ${c}`} strokeDashoffset={c*0.25}
|
|
transform={`rotate(-90 ${size/2} ${size/2})`}
|
|
strokeLinecap="round"/>
|
|
{label && <text x={size/2} y={size/2+1} textAnchor="middle" fontSize="13" fontWeight="600" fill="var(--fg-0)" fontFamily="var(--font-mono)">{label}</text>}
|
|
</svg>
|
|
);
|
|
};
|
|
|
|
// Radial gauge (for speed / soc)
|
|
const Gauge = ({ value = 0.6, size = 100, color = "var(--accent)", label, sub }) => {
|
|
const r = size/2 - 8;
|
|
const c = Math.PI * r; // half circle
|
|
return (
|
|
<svg width={size} height={size*0.65} viewBox={`0 0 ${size} ${size*0.65}`}>
|
|
<path d={`M 8 ${size/2} A ${r} ${r} 0 0 1 ${size-8} ${size/2}`}
|
|
fill="none" stroke="var(--bg-3)" strokeWidth="6" strokeLinecap="round"/>
|
|
<path d={`M 8 ${size/2} A ${r} ${r} 0 0 1 ${size-8} ${size/2}`}
|
|
fill="none" stroke={color} strokeWidth="6" strokeLinecap="round"
|
|
strokeDasharray={`${c*value} ${c}`}/>
|
|
<text x={size/2} y={size/2 - 4} textAnchor="middle" fontSize="18" fontWeight="600" fill="var(--fg-0)" fontFamily="var(--font-mono)">{label}</text>
|
|
{sub && <text x={size/2} y={size/2 + 10} textAnchor="middle" fontSize="9" fill="var(--fg-3)" letterSpacing="0.1em">{sub}</text>}
|
|
</svg>
|
|
);
|
|
};
|
|
|
|
// Stacked horizontal bar (battery + h2)
|
|
const Stack = ({ segs, w = 200, h = 8 }) => {
|
|
const total = segs.reduce((a, s) => a + s.v, 0) || 1;
|
|
let x = 0;
|
|
return (
|
|
<svg width={w} height={h} style={{display: "block"}}>
|
|
{segs.map((s, i) => {
|
|
const sw = w * (s.v/total);
|
|
const r = <rect key={i} x={x} y="0" width={sw} height={h} fill={s.color || "var(--accent)"}/>;
|
|
x += sw;
|
|
return r;
|
|
})}
|
|
</svg>
|
|
);
|
|
};
|
|
|
|
// Sample data generators (deterministic)
|
|
const seed = (s) => () => { s = (s * 9301 + 49297) % 233280; return s / 233280; };
|
|
|
|
const genSpeed = () => {
|
|
const r = seed(12);
|
|
const out = [];
|
|
let v = 50;
|
|
for (let i = 0; i < 60; i++) {
|
|
v += (r() - 0.5) * 12;
|
|
v = Math.max(0, Math.min(95, v));
|
|
out.push(v);
|
|
}
|
|
return out;
|
|
};
|
|
const genSoc = () => {
|
|
const out = [];
|
|
let v = 92;
|
|
for (let i = 0; i < 60; i++) {
|
|
v -= 0.08 + Math.random()*0.4;
|
|
out.push(Math.max(15, v));
|
|
}
|
|
return out;
|
|
};
|
|
const genH2 = () => {
|
|
const out = [];
|
|
let v = 4.8;
|
|
for (let i = 0; i < 60; i++) {
|
|
v -= 0.005 + Math.random()*0.04;
|
|
out.push(Math.max(0.5, v));
|
|
}
|
|
return out;
|
|
};
|
|
|
|
window.LineChart = LineChart;
|
|
window.Bars = Bars;
|
|
window.Donut = Donut;
|
|
window.Gauge = Gauge;
|
|
window.Stack = Stack;
|
|
window.genSpeed = genSpeed;
|
|
window.genSoc = genSoc;
|
|
window.genH2 = genH2;
|