Files
oneos-truck-date-prototype/artboards/esg.jsx
kkfluous b2d0016a0d
All checks were successful
ci/woodpecker/manual/woodpecker Pipeline was successful
init: 羚牛车辆数据中心原型 + 部署配置
- 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 探活
2026-04-28 15:12:32 +08:00

497 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// artboard-esg.jsx — ESG · Carbon Reduction Cockpit (light green theme)
// Mirrors reference: white ground, multi-tier green, China choropleth + KPIs
const ChinaMapMini = ({ w = 480, h = 360 }) => {
// Simplified provincial silhouette — abstract, recognisable. Levels keyed by data.
// Each path is roughly positioned on a 480x360 canvas of mainland.
const G = { 4: "#1F8B4C", 3: "#4FB46E", 2: "#9DD3A6", 1: "#D7EBD2", 0: "#EEF5EC" };
const provs = [
// [name, level, polygon]
{n:"新疆", l:1, d:"M40 90 L130 70 L150 110 L120 160 L60 150 Z"},
{n:"西藏", l:0, d:"M70 160 L150 140 L180 180 L140 220 L80 200 Z"},
{n:"青海", l:1, d:"M150 130 L210 130 L210 175 L160 180 Z"},
{n:"甘肃", l:2, d:"M180 100 L240 80 L260 120 L220 140 L200 130 Z"},
{n:"内蒙", l:3, d:"M180 60 L320 40 L350 70 L330 95 L260 100 L210 90 Z"},
{n:"宁夏", l:1, d:"M225 110 L245 100 L255 125 L235 130 Z"},
{n:"陕西", l:3, d:"M245 110 L280 105 L290 160 L260 180 L245 145 Z"},
{n:"山西", l:2, d:"M285 90 L310 88 L320 145 L295 150 Z"},
{n:"河北", l:2, d:"M310 75 L355 70 L365 115 L325 130 L315 100 Z"},
{n:"北京", l:4, d:"M335 82 L355 80 L355 95 L338 95 Z"},
{n:"天津", l:3, d:"M358 92 L370 92 L370 105 L358 105 Z"},
{n:"辽宁", l:2, d:"M360 60 L405 55 L420 90 L385 105 L362 88 Z"},
{n:"吉林", l:1, d:"M395 35 L440 30 L450 65 L410 70 Z"},
{n:"黑龙江", l:1, d:"M390 5 L460 0 L470 35 L420 40 Z"},
{n:"山东", l:3, d:"M325 130 L380 125 L390 165 L335 165 Z"},
{n:"河南", l:3, d:"M280 155 L330 150 L335 195 L290 200 Z"},
{n:"江苏", l:3, d:"M345 165 L390 165 L395 200 L350 205 Z"},
{n:"上海", l:4, d:"M390 195 L405 195 L405 210 L390 210 Z"},
{n:"安徽", l:3, d:"M315 195 L350 200 L355 235 L320 235 Z"},
{n:"浙江", l:4, d:"M370 210 L400 210 L405 245 L375 245 Z"},
{n:"湖北", l:3, d:"M270 195 L320 200 L320 235 L275 230 Z"},
{n:"四川", l:2, d:"M195 175 L260 170 L270 230 L210 225 L195 205 Z"},
{n:"重庆", l:2, d:"M250 215 L275 210 L275 230 L255 232 Z"},
{n:"贵州", l:2, d:"M225 235 L275 235 L275 265 L235 265 Z"},
{n:"云南", l:2, d:"M170 240 L235 235 L240 285 L185 290 L160 270 Z"},
{n:"湖南", l:3, d:"M275 235 L320 235 L320 270 L280 270 Z"},
{n:"江西", l:3, d:"M320 235 L360 235 L365 275 L325 275 Z"},
{n:"福建", l:3, d:"M360 245 L395 245 L395 285 L360 280 Z"},
{n:"广东", l:4, d:"M270 270 L355 275 L355 305 L280 305 Z"},
{n:"广西", l:2, d:"M210 270 L275 270 L275 305 L215 305 Z"},
{n:"海南", l:1, d:"M250 320 L275 320 L275 340 L250 340 Z"},
{n:"台湾", l:0, d:"M395 270 L410 270 L410 300 L398 300 Z"},
];
return (
<svg viewBox={`0 0 ${w} ${h}`} width="100%" height="100%" style={{display:"block"}}>
<defs>
<pattern id="esgGrid" width="20" height="20" patternUnits="userSpaceOnUse">
<path d="M 20 0 L 0 0 0 20" fill="none" stroke="#E8F1E5" strokeWidth="0.5"/>
</pattern>
</defs>
<rect x="0" y="0" width={w} height={h} fill="url(#esgGrid)"/>
{/* sea-line decoration */}
<path d="M 410 60 Q 440 130 430 220 Q 420 280 380 320" fill="none" stroke="#D5EBEB" strokeWidth="1" strokeDasharray="3 3"/>
{provs.map((p,i) => (
<g key={i}>
<path d={p.d} fill={G[p.l]} stroke="#FFFFFF" strokeWidth="1.2"/>
</g>
))}
{/* highlighted city marker — Beijing */}
<g transform="translate(345 88)">
<circle r="9" fill="#1F8B4C" opacity="0.18"/>
<circle r="4" fill="#1F8B4C"/>
<circle r="2" fill="#FFFFFF"/>
</g>
{/* Shanghai */}
<g transform="translate(398 202)">
<circle r="3.5" fill="#1F8B4C"/>
</g>
{/* Guangzhou */}
<g transform="translate(310 296)">
<circle r="3.5" fill="#1F8B4C"/>
</g>
{/* Compass / scale */}
<g transform="translate(20 320)" fontFamily="JetBrains Mono" fontSize="9" fill="#5C7A66">
<line x1="0" y1="0" x2="40" y2="0" stroke="#5C7A66" strokeWidth="1"/>
<line x1="0" y1="-3" x2="0" y2="3" stroke="#5C7A66" strokeWidth="1"/>
<line x1="40" y1="-3" x2="40" y2="3" stroke="#5C7A66" strokeWidth="1"/>
<text x="20" y="14" textAnchor="middle">800 km</text>
</g>
</svg>
);
};
// Curve helpers
const ESGSpark = ({ data, w, h, color = "#1F8B4C", fill = true, baseline = 0 }) => {
const max = Math.max(...data) * 1.1, min = Math.min(...data, 0);
const range = max - min || 1;
const pts = data.map((v,i) => `${(i/(data.length-1))*w},${h - ((v-min)/range)*(h-baseline) - baseline}`);
const d = "M" + pts.join(" L");
const fillD = d + ` L${w},${h} L0,${h} Z`;
return (
<svg width={w} height={h} style={{display:"block"}}>
{fill && <path d={fillD} fill={color} opacity="0.12"/>}
<path d={d} fill="none" stroke={color} strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
);
};
const ESGBars = ({ data, w, h, color = "#1F8B4C", labels }) => {
const max = Math.max(...data) * 1.15;
const bw = w / data.length * 0.62;
const gap = w / data.length * 0.38;
return (
<svg width={w} height={h} style={{display:"block"}}>
{/* y gridlines */}
{[0, 0.25, 0.5, 0.75, 1].map((p,i) => (
<line key={i} x1="0" y1={h - p*h*0.85 - 14} x2={w} y2={h - p*h*0.85 - 14} stroke="#E8F1E5" strokeWidth="1"/>
))}
{data.map((v,i) => {
const bh = (v / max) * (h * 0.85);
const x = i * (bw + gap) + gap/2;
const y = h - bh - 14;
return (
<g key={i}>
<rect x={x} y={y} width={bw} height={bh} fill={color} rx="1"/>
{labels && <text x={x + bw/2} y={h - 2} textAnchor="middle" fontSize="9" fill="#8FA897" fontFamily="JetBrains Mono">{labels[i]}</text>}
</g>
);
})}
</svg>
);
};
const DonutSeg = ({ size = 140, segments, label }) => {
const r = size/2 - 12, cx = size/2, cy = size/2;
const total = segments.reduce((a,s) => a + s.v, 0);
let acc = 0;
return (
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
<circle cx={cx} cy={cy} r={r} fill="none" stroke="#F0F5EE" strokeWidth="14"/>
{segments.map((s,i) => {
const start = (acc / total) * Math.PI * 2 - Math.PI/2;
acc += s.v;
const end = (acc / total) * Math.PI * 2 - Math.PI/2;
const large = (end - start) > Math.PI ? 1 : 0;
const x1 = cx + r * Math.cos(start), y1 = cy + r * Math.sin(start);
const x2 = cx + r * Math.cos(end), y2 = cy + r * Math.sin(end);
return (
<path key={i} d={`M ${x1} ${y1} A ${r} ${r} 0 ${large} 1 ${x2} ${y2}`}
stroke={s.c} strokeWidth="14" fill="none" strokeLinecap="butt"/>
);
})}
<text x={cx} y={cy - 4} textAnchor="middle" fontSize="10" fill="#8FA897">合计</text>
<text x={cx} y={cy + 14} textAnchor="middle" fontSize="20" fontWeight="600" fill="#1A2A1F" fontFamily="JetBrains Mono">{label}</text>
</svg>
);
};
const ArtboardESG = () => {
// mock data
const monthlyReduction = [0.95, 1.10, 1.32, 1.55, 1.42, 1.38, 0, 0, 0, 0, 0, 0]; // 吨
const monthLabels = ["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"];
const mileageMonthly = [120, 145, 168, 152, 195, 180, 0,0,0,0,0,0];
const h2Monthly = [180, 220, 255, 235, 290, 270, 0,0,0,0,0,0];
const vehicles = [
{p:"浙F·8A03F", km:"18,250 km", h2:"257 m³", reduction:"24.38 kg", revenue:"18.785 元"},
{p:"浙F·2C57G", km:"5,367 km", h2:"75 m³", reduction:"7.13 kg", revenue:"181.785 元"},
{p:"浙F·9D14B", km:"45,000 km", h2:"234 m³", reduction:"12.82 kg", revenue:"194.382 元"},
{p:"浙F·6E72H", km:"55,387 km", h2:"218 m³", reduction:"17.94 kg", revenue:"152.578 元"},
{p:"浙F·1B49K", km:"55,925 km", h2:"203 m³", reduction:"17.87 kg", revenue:"148.392 元"},
{p:"浙F·4F88M", km:"887,820 km",h2:"152 m³", reduction:"9.6 kg", revenue:"73.627 元"},
{p:"浙F·7G31N", km:"3,762 km", h2:"134 m³", reduction:"13.91 kg", revenue:"66.991 元"},
{p:"浙F·3H56P", km:"30,058 km", h2:"125 m³", reduction:"13.87 kg", revenue:"82.578 元"},
{p:"浙F·5J92Q", km:"3,701 km", h2:"121 m³", reduction:"8.49 kg", revenue:"103.928 元"},
{p:"浙F·8K27R", km:"5,829 km", h2:"165 m³", reduction:"15.62 kg", revenue:"76.354 元"},
{p:"浙F·2L68S", km:"73,587 km", h2:"185 m³", reduction:"4.85 kg", revenue:"54.812 元"},
{p:"浙F·9M03T", km:"38,747 km", h2:"168 m³", reduction:"11.57 kg", revenue:"72.836 元"},
];
const trades = [
{ex:"上海环境能源交易所", item:"SHEA", price:"74.28", region:"中国·上海"},
{ex:"湖北碳排放权交易中心", item:"CCER", price:"39.33", region:"中国·武汉"},
{ex:"全国碳市场自愿减排", item:"CCER", price:"86.55", region:"全国"},
{ex:"福建海峡股权交易中心", item:"碳排放配额", price:"25", region:"中国·福州"},
{ex:"天津排放权交易所", item:"碳排放配额", price:"73.60", region:"中国·天津"},
{ex:"广东省碳排放权交易所", item:"碳排放配额", price:"82.50", region:"中国·广州"},
];
const fleetMix = [
{n:"4.5吨冷链车", v:36.2, c:"#1F8B4C"},
{n:"18吨重卡", v:4.0, c:"#9DD3A6"},
{n:"49吨牵引车", v:21.7, c:"#4FB46E"},
{n:"18吨厢式物流车",v:29.5, c:"#76C18B"},
{n:"4.5吨货车", v:6.6, c:"#C5E2BD"},
{n:"客车", v:2.1, c:"#E5F1DF"},
];
return (
<div className="app" data-theme="light" style={{background:"#F2F5EF", colorScheme:"light"}}>
<Sidebar active="esg"/>
<div style={{flex:1, display:"flex", flexDirection:"column", minWidth:0, position:"relative", zIndex:1}}>
{/* Top brand bar */}
<div style={{
height:48, flex:"0 0 48px",
background:"#FFFFFF",
borderBottom:"1px solid var(--border-1)",
display:"flex", alignItems:"center", padding:"0 20px", gap:16,
}}>
<div className="mid gap-2">
<img src="assets/logo_light.svg" alt="羚牛氢能 Lingniu" style={{height:32, display:"block"}}/>
<div style={{fontSize:9, color:"#5C7A66", letterSpacing:"0.12em", fontFamily:"JetBrains Mono", paddingLeft:6, borderLeft:"1px solid #D4E2D5", marginLeft:4}}>
HYDROGEN<br/>MOBILITY
</div>
</div>
<div style={{flex:1, textAlign:"center", fontWeight:500, color:"#1F8B4C", letterSpacing:"0.06em", fontSize:18, fontFamily:"IBM Plex Sans"}}>
Lingniu ESG Link
</div>
<div className="mono" style={{fontSize:11, color:"#1F8B4C", background:"#DCEFD7", padding:"4px 10px", borderRadius:4, border:"1px solid #B5DDB1"}}>2026-04-28 周二 12:15:13</div>
<div className="icon-btn" style={{color:"#5C7A66"}}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9c0 .61.36 1.16.91 1.39l.13.05A1.65 1.65 0 0 0 21 13"/></svg>
</div>
</div>
{/* Body grid */}
<div className="scroll" style={{flex:1, padding:14, display:"grid", gridTemplateColumns:"380px 1fr 380px", gap:12, gridAutoRows:"min-content"}}>
{/* ── LEFT COLUMN ── */}
<div className="col gap-3" style={{gap:12}}>
{/* Two top KPIs: emissions & H₂ */}
<div style={{display:"grid", gridTemplateColumns:"1fr 1fr", gap:10}}>
<div className="panel" style={{padding:14}}>
<div className="between">
<div>
<div style={{fontSize:11, color:"#5C7A66"}}>当日减碳量</div>
<div className="mono" style={{fontSize:22, fontWeight:600, color:"#1F8B4C", marginTop:4}}>29486.78<span style={{fontSize:11, marginLeft:3, color:"#5C7A66", fontWeight:400}}>kg</span></div>
</div>
<svg width="34" height="34" viewBox="0 0 40 40" fill="none">
<circle cx="20" cy="20" r="18" fill="#DCEFD7"/>
<path d="M20 8 c-4 4 -8 8 -8 14 a8 8 0 0 0 16 0 c0 -6 -4 -10 -8 -14z" fill="#1F8B4C"/>
</svg>
</div>
</div>
<div className="panel" style={{padding:14}}>
<div className="between">
<div>
<div style={{fontSize:11, color:"#5C7A66"}}>当日H₂用量</div>
<div className="mono" style={{fontSize:22, fontWeight:600, color:"#1F8B4C", marginTop:4}}>974.7<span style={{fontSize:11, marginLeft:3, color:"#5C7A66", fontWeight:400}}>kg</span></div>
</div>
<svg width="34" height="34" viewBox="0 0 40 40" fill="none">
<circle cx="20" cy="20" r="18" fill="#D5EBEB"/>
<circle cx="14" cy="22" r="3" fill="#2E8C8C"/>
<circle cx="22" cy="16" r="2.4" fill="#2E8C8C"/>
<circle cx="26" cy="24" r="3.6" fill="#2E8C8C"/>
<circle cx="18" cy="14" r="2" fill="#2E8C8C"/>
</svg>
</div>
</div>
</div>
{/* Annual cumulative reduction — hero card */}
<div className="panel" style={{
padding:16, position:"relative", overflow:"hidden",
background:"linear-gradient(135deg, #DCEFD7 0%, #FFFFFF 70%)",
border:"1px solid #B5DDB1",
}}>
<div style={{fontSize:11, color:"#5C7A66"}}>今年累计减碳</div>
<div style={{display:"flex", alignItems:"baseline", gap:6, marginTop:6}}>
<span className="mono" style={{fontSize:34, fontWeight:600, color:"#1F8B4C", letterSpacing:"-0.02em"}}>4567.14</span>
<span style={{fontSize:13, color:"#5C7A66"}}></span>
</div>
<div style={{fontSize:10, color:"#5C7A66", marginTop:4, display:"flex", alignItems:"center", gap:6}}>
<span style={{display:"inline-block", width:14, height:1, background:"#1F8B4C"}}/>
相当于种植 18.5 万棵树
</div>
{/* abstract tree silhouette */}
<svg width="100%" height="50" viewBox="0 0 280 50" style={{marginTop:10, opacity:0.55}}>
{[...Array(28)].map((_,i) => {
const x = i*10 + 4;
const heights = [22, 30, 26, 34, 28, 32, 24, 36, 30, 28, 32, 26, 34, 30];
const h = heights[i % heights.length];
return (
<g key={i} transform={`translate(${x} ${50-h})`}>
<path d={`M 4 ${h} L 0 ${h-h*0.7} L 3 ${h-h*0.7} L -1 ${h-h*0.85} L 2 ${h-h*0.85} L 0 ${h} z M 4 ${h} L 8 ${h-h*0.7} L 5 ${h-h*0.7} L 9 ${h-h*0.85} L 6 ${h-h*0.85} L 8 ${h} z`} fill="#1F8B4C" opacity={0.4 + (i%3)*0.18}/>
<rect x="3.5" y={h-2} width="1" height="2" fill="#5C7A66"/>
</g>
);
})}
</svg>
</div>
{/* Monthly reduction bars */}
<div className="panel" style={{padding:14}}>
<div className="between" style={{marginBottom:10}}>
<span style={{fontWeight:600, fontSize:13, color:"#1A2A1F"}}>月度碳减排</span>
<span className="chip" style={{fontSize:10}}>单位 · </span>
</div>
<ESGBars data={monthlyReduction} w={350} h={150} labels={monthLabels}/>
</div>
{/* Monthly mileage / H2 */}
<div className="panel" style={{padding:14}}>
<div className="between" style={{marginBottom:10}}>
<span style={{fontWeight:600, fontSize:13, color:"#1A2A1F"}}>月度行驶里程 & 用氢量</span>
</div>
<div className="mid gap-3" style={{fontSize:10, color:"#5C7A66", marginBottom:8}}>
<span className="mid gap-1"><span className="dot" style={{background:"#1F8B4C", width:8, height:8, borderRadius:1}}/>用氢量</span>
<span className="mid gap-1"><span className="dot" style={{background:"#9DD3A6", width:8, height:8, borderRadius:1}}/>行驶里程</span>
<span style={{marginLeft:"auto", fontFamily:"JetBrains Mono"}}>kg / km</span>
</div>
<svg width="350" height="135" viewBox="0 0 350 135" style={{display:"block"}}>
{[0, 0.25, 0.5, 0.75, 1].map((p,i) => (
<line key={i} x1="0" y1={120 - p*100} x2="350" y2={120 - p*100} stroke="#E8F1E5" strokeWidth="1"/>
))}
{[0, 0.25, 0.5, 0.75, 1].map((p,i) => (
<text key={i} x="0" y={124 - p*100} fontSize="9" fill="#8FA897" fontFamily="JetBrains Mono">{Math.round(p*400)}</text>
))}
{/* Curves */}
{(() => {
const m = (arr, max) => arr.map((v,i)=>`${(i/(arr.length-1))*350},${120 - (v/max)*100}`);
const p1 = "M" + m(h2Monthly, 400).join(" L");
const p2 = "M" + m(mileageMonthly, 250).join(" L");
return (
<>
<path d={p1 + " L350,120 L0,120 z"} fill="#1F8B4C" opacity="0.13"/>
<path d={p1} fill="none" stroke="#1F8B4C" strokeWidth="2"/>
<path d={p2} fill="none" stroke="#9DD3A6" strokeWidth="2"/>
</>
);
})()}
{monthLabels.map((l,i) => (
<text key={i} x={(i/(monthLabels.length-1))*350} y="132" textAnchor="middle" fontSize="8" fill="#8FA897" fontFamily="JetBrains Mono">{l}</text>
))}
</svg>
</div>
</div>
{/* ── CENTER COLUMN ── */}
<div className="col" style={{gap:12}}>
{/* Map panel */}
<div className="panel" style={{padding:14, paddingBottom:18}}>
<div className="between">
<div className="mid gap-2">
<span style={{fontWeight:600, fontSize:14, color:"#1A2A1F"}}>羚牛全国车辆信息</span>
<span className="chip accent" style={{fontSize:10}}>加氢站</span>
</div>
<span className="chip" style={{fontSize:10, color:"#1F8B4C", borderColor:"#B5DDB1", background:"#DCEFD7"}}>实时反馈</span>
</div>
<div style={{position:"relative", marginTop:10, height:380}}>
<ChinaMapMini w={580} h={380}/>
{/* Overlay info card */}
<div style={{
position:"absolute", top:30, left:200,
background:"rgba(255,255,255,0.95)", padding:"10px 14px",
borderRadius:6, border:"1px solid #B5DDB1", fontSize:11, color:"#2E4234",
boxShadow:"0 4px 16px rgba(31,80,46,.08)",
}}>
<div style={{fontSize:11, color:"#1F8B4C", fontWeight:600}}>呼和浩特市钢铁工业园区</div>
<div className="mid" style={{gap:14, marginTop:6, fontFamily:"JetBrains Mono", fontSize:10}}>
<div><div style={{color:"#8FA897"}}>GPS实时数</div><div style={{color:"#1A2A1F", fontWeight:600}}>17</div></div>
<div><div style={{color:"#8FA897"}}>当日总减碳</div><div style={{color:"#1A2A1F", fontWeight:600}}>2469.62 kg</div></div>
<div><div style={{color:"#8FA897"}}>当日加氢量</div><div style={{color:"#1A2A1F", fontWeight:600}}>9.31 kg</div></div>
<div><div style={{color:"#8FA897"}}>当日里程</div><div style={{color:"#1A2A1F", fontWeight:600}}>724.6 kg</div></div>
</div>
</div>
{/* Legend */}
<div style={{position:"absolute", bottom:16, left:16, fontSize:10, color:"#5C7A66"}}>
<div style={{marginBottom:4, fontWeight:600, color:"#2E4234"}}>车辆数</div>
{[
{l:"≥ 300 辆", c:"#1F8B4C"},
{l:"100300 辆", c:"#4FB46E"},
{l:"50100 辆", c:"#9DD3A6"},
{l:"< 50 辆", c:"#D7EBD2"},
].map((x,i) => (
<div key={i} className="mid gap-1" style={{marginTop:2}}>
<span style={{display:"inline-block", width:14, height:10, background:x.c, border:"1px solid #FFFFFF"}}/>
<span style={{fontFamily:"JetBrains Mono"}}>{x.l}</span>
</div>
))}
</div>
</div>
</div>
{/* Carbon trades table */}
<div className="panel" style={{padding:0}}>
<div className="between" style={{padding:"12px 14px", borderBottom:"1px solid var(--border-1)"}}>
<span style={{fontWeight:600, fontSize:13, color:"#1A2A1F"}}>碳交易行情</span>
<span className="chip" style={{fontSize:10}}>实时报价</span>
</div>
<table className="tbl" style={{fontSize:11}}>
<thead>
<tr><th>交易所</th><th></th><th> (RMB)</th><th></th></tr>
</thead>
<tbody>
{trades.map((t,i) => (
<tr key={i}>
<td style={{color:"#1A2A1F"}}>{t.ex}</td>
<td className="mono" style={{color:"#1F8B4C", fontWeight:600}}>{t.item}</td>
<td className="mono" style={{color:"#1A2A1F", fontWeight:600}}>{t.price}</td>
<td style={{color:"#5C7A66"}}>{t.region}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* ── RIGHT COLUMN ── */}
<div className="col" style={{gap:12}}>
{/* Two top KPIs: vehicle total & cumulative mileage */}
<div style={{display:"grid", gridTemplateColumns:"1fr 1fr", gap:10}}>
<div className="panel" style={{padding:14}}>
<div className="between">
<div>
<div style={{fontSize:11, color:"#5C7A66"}}>车辆总数</div>
<div className="mono" style={{fontSize:22, fontWeight:600, color:"#1F8B4C", marginTop:4}}>1006<span style={{fontSize:11, marginLeft:3, color:"#5C7A66", fontWeight:400}}></span></div>
</div>
<svg width="34" height="34" viewBox="0 0 40 40" fill="none">
<circle cx="20" cy="20" r="18" fill="#DCEFD7"/>
<rect x="11" y="17" width="18" height="10" rx="2" fill="#1F8B4C"/>
<circle cx="14" cy="29" r="2" fill="#1F8B4C"/>
<circle cx="26" cy="29" r="2" fill="#1F8B4C"/>
</svg>
</div>
</div>
<div className="panel" style={{padding:14}}>
<div className="between">
<div>
<div style={{fontSize:11, color:"#5C7A66"}}>当日行驶里程</div>
<div className="mono" style={{fontSize:22, fontWeight:600, color:"#1F8B4C", marginTop:4}}>64508.42<span style={{fontSize:11, marginLeft:3, color:"#5C7A66", fontWeight:400}}>km</span></div>
</div>
<svg width="34" height="34" viewBox="0 0 40 40" fill="none">
<circle cx="20" cy="20" r="18" fill="#D5EBEB"/>
<path d="M10 22 a10 10 0 0 1 20 0" fill="none" stroke="#2E8C8C" strokeWidth="2"/>
<path d="M20 22 L25 14" stroke="#2E8C8C" strokeWidth="2" strokeLinecap="round"/>
<circle cx="20" cy="22" r="1.5" fill="#2E8C8C"/>
</svg>
</div>
</div>
</div>
{/* Vehicle live monitor table */}
<div className="panel" style={{padding:0, flex:1, minHeight:380, display:"flex", flexDirection:"column"}}>
<div className="between" style={{padding:"12px 14px", borderBottom:"1px solid var(--border-1)"}}>
<span style={{fontWeight:600, fontSize:13, color:"#1A2A1F"}}>车辆实时监控</span>
<span className="chip ok" style={{fontSize:10}}>· LIVE</span>
</div>
<div className="scroll" style={{flex:1}}>
<table className="tbl" style={{fontSize:11}}>
<thead>
<tr>
<th>车牌号</th><th></th><th></th><th></th>
</tr>
</thead>
<tbody>
{vehicles.map((v,i) => (
<tr key={i}>
<td className="mono" style={{color:"#1A2A1F", fontWeight:600}}>{v.p}</td>
<td className="mono" style={{color:"#5C7A66"}}>{v.km}</td>
<td className="mono" style={{color:"#5C7A66"}}>{v.h2}</td>
<td className="mono" style={{color:"#1F8B4C", fontWeight:600}}>{v.reduction}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Fleet mix donut */}
<div className="panel" style={{padding:14}}>
<div className="between" style={{marginBottom:10}}>
<span style={{fontWeight:600, fontSize:13, color:"#1A2A1F"}}>车型结构分析</span>
</div>
<div className="mid gap-3" style={{alignItems:"center"}}>
<DonutSeg size={130} segments={fleetMix} label="1006"/>
<div style={{flex:1, display:"flex", flexDirection:"column", gap:5, fontSize:11}}>
{fleetMix.map((f,i) => (
<div key={i} className="between">
<span className="mid gap-2">
<span style={{width:10, height:10, borderRadius:2, background:f.c}}/>
<span style={{color:"#5C7A66"}}>{f.n}</span>
</span>
<span className="mono" style={{color:"#1A2A1F", fontWeight:600}}>{f.v}%</span>
</div>
))}
</div>
</div>
</div>
</div>
</div>
{/* Footer */}
<div style={{
height:24, flex:"0 0 24px",
borderTop:"1px solid var(--border-1)", background:"#FFFFFF",
display:"flex", alignItems:"center", justifyContent:"center", gap:14,
fontSize:10, color:"#8FA897",
}}>
<span>© 2026 羚牛氢能 · Lingniu Hydrogen Mobility · All Rights Reserved</span>
<span style={{color:"#1F8B4C"}}>· API 接口处理 · Build v4.2.0-stable</span>
</div>
</div>
</div>
);
};
window.ArtboardESG = ArtboardESG;