Files
oneos-truck-date-prototype/artboards/detail.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

370 lines
23 KiB
JavaScript

// artboard-detail.jsx — Single vehicle deep detail · Asset-management view
const ArtboardDetail = () => {
const vehicles = window.VEHICLES || [];
const v = vehicles.find(x => x.id === "浙F03980F") || vehicles[0];
if (!v) return null;
return (
<div className="app">
<Sidebar active="fleet"/>
<div style={{flex:1, display:"flex", flexDirection:"column", minWidth:0, position:"relative", zIndex:1}}>
<Topbar
crumbs={["车辆列表", v.plate, "详情"]}
kpis={[]}
showSearch={false}
/>
<div className="scroll" style={{flex:1, padding:16, display:"grid", gridTemplateColumns:"1fr 1fr 1fr", gridAutoRows:"min-content", gap:12}}>
{/* Header card spanning 3 */}
<div className="panel" style={{gridColumn:"1 / -1", padding:16}}>
<div className="between">
<div className="mid gap-3">
<div style={{width:60, height:60, borderRadius:8, background:"var(--bg-2)", border:"1px solid var(--border-2)", display:"grid", placeItems:"center", color:"var(--accent)"}}>
<Icon name="car" size={30}/>
</div>
<div>
<div className="mid gap-2">
<span className="mono strong" style={{fontSize:22, fontWeight:600}}>{v.plate}</span>
<span className="chip" style={{
background: v.asset === "leasing" ? "rgba(46,140,140,.15)" : v.asset === "abnormal" ? "var(--danger-soft)" : "var(--accent-soft)",
color: v.asset === "leasing" ? "var(--info)" : v.asset === "abnormal" ? "var(--danger)" : "var(--accent)",
}}>
<span className={"dot " + (v.asset === "abnormal" ? "danger" : v.asset === "leasing" ? "info" : "ok")}/>
{v.asset === "leasing" ? "租赁" : v.asset === "abnormal" ? "异常" : "在库"}
</span>
<span className="chip" style={{background: v.own === "self" ? "rgba(31,139,76,.10)" : "rgba(122,140,46,.12)", color: v.own === "self" ? "var(--accent)" : "#7A8C2E"}}>
{v.own === "self" ? "自有" : "外租"}
</span>
<span className="chip accent"><Icon name="h2" size={11}/> H₂ {v.h2} MPa</span>
</div>
<div className="muted" style={{fontSize:12, marginTop:4}}>VIN {v.vin} · {v.city} · 等级 {v.grade} · 状态时长 {v.statusDays}</div>
<div className="mid gap-2" style={{marginTop:6, fontSize:11}}>
<span className="muted">数据来源</span>
<SourceBadge src={v.src} size="md"/>
<span className="muted mono" style={{fontSize:10}}>TBOX(GB/T 32960-2016) · JT/T 808-2019 · JT/T 1078 视频</span>
<span className={"chip " + (v.gps === "online" ? "ok" : "")} style={{fontSize:10}}>
<span className={"dot " + (v.gps === "online" ? "ok" : "idle")} style={{width:4, height:4}}/>
{v.gps === "online" ? "在线 · 上行 218ms" : "GPS离线"}
</span>
</div>
</div>
</div>
<div className="mid gap-2">
<button className="btn"><Icon name="route" size={13}/> 轨迹</button>
<button className="btn"><Icon name="history" size={13}/> 历史</button>
<button className="btn"><Icon name="bell" size={13}/> 告警</button>
<button className="btn primary"><Icon name="pin" size={13}/> 定位</button>
</div>
</div>
<div style={{marginTop:14, display:"grid", gridTemplateColumns:"repeat(6, 1fr)", gap:0, borderTop:"1px solid var(--border-1)", paddingTop:14}}>
{[
{l:"累计里程", val:v.totalKm.toLocaleString(), u:"km"},
{l:"今日里程", val:"248", u:"km"},
{l:"距下次保养", val:v.kmToMaint.toLocaleString(), u:"km"},
{l:"今日能耗", val:"18.4", u:"kWh/100km"},
{l:"H₂消耗", val:"1.02", u:"kg/100km"},
{l:"车辆评级", val:v.grade, u:"级"},
].map((k,i)=>(
<div key={i} style={{borderRight: i < 5 ? "1px solid var(--border-1)": "none", padding:"0 16px"}}>
<div className="eyebrow" style={{marginBottom:6}}>{k.l}</div>
<div><span className="mono strong" style={{fontSize:22, fontWeight:600}}>{k.val}</span><span className="muted mono" style={{fontSize:11, marginLeft:4}}>{k.u}</span></div>
</div>
))}
</div>
</div>
{/* 资产档案 */}
<div className="panel">
<div className="panel-head">
<Icon name="layers" size={13} style={{color:"var(--accent)"}}/>
<span className="title">资产档案</span>
</div>
<div style={{padding:"14px 16px"}}>
<div className="col gap-2" style={{fontSize:11}}>
<div className="between"><span className="muted">车牌号</span><span className="mono strong">{v.plate}</span></div>
<div className="between"><span className="muted">VIN/车架号</span><span className="mono" style={{fontSize:10}}>{v.vin}</span></div>
{v.fleetCode && <div className="between"><span className="muted">车辆编号</span><span className="mono strong">{v.fleetCode}</span></div>}
<div className="between"><span className="muted">运营城市</span><span>{v.city}</span></div>
<div className="between"><span className="muted">所属公司</span><span style={{fontSize:10, textAlign:"right"}}>{v.ownCompany}</span></div>
<div className="between"><span className="muted">车辆等级</span><span className="strong">{v.grade}</span></div>
<div className="between"><span className="muted">归属</span><span className="strong">{v.own === "self" ? "" : ""}</span></div>
<div className="between"><span className="muted">停车场</span><span>{v.parking}</span></div>
<div className="between"><span className="muted">资产状态</span><span className="strong" style={{color: v.asset === "abnormal" ? "var(--danger)" : v.asset === "leasing" ? "var(--info)" : "var(--accent)"}}>
{v.asset === "leasing" ? "租赁" : v.asset === "abnormal" ? "异常" : "在库"} · {v.statusDays}
</span></div>
<div className="between"><span className="muted">营运状态</span><span className="strong">
{v.op === "operating" ? "运营中" : v.op === "suspended" ? "停运" : "待整备"}
</span></div>
</div>
</div>
</div>
{/* 业务关系 */}
<div className="panel">
<div className="panel-head">
<Icon name="user" size={13} style={{color:"var(--info)"}}/>
<span className="title">业务关系</span>
</div>
<div style={{padding:"14px 16px"}}>
<div className="col gap-2" style={{fontSize:11}}>
<div className="between"><span className="muted">业务部门</span>
<span className="mid gap-1">
<span style={{width:8, height:8, background:v.deptColor, borderRadius:1, display:"inline-block"}}/>
<span className="strong">{v.deptName}</span>
</span>
</div>
<div className="between"><span className="muted">业务负责人</span><span className="strong">{v.deptLead}</span></div>
<div className="between"><span className="muted">客户全名</span><span className="strong" style={{textAlign:"right", maxWidth:160}}>{v.customer}</span></div>
{v.own === "lease" && <div className="between"><span className="muted">租赁公司</span><span style={{fontSize:10, textAlign:"right"}}>{v.company}</span></div>}
{v.contractNo && <>
<div className="between"><span className="muted">合同编号</span><span className="mono" style={{fontSize:10}}>{v.contractNo}</span></div>
<div className="between"><span className="muted">交车里程</span><span className="mono">{v.handoverKm?.toLocaleString()} km</span></div>
{v.returnKm != null && <div className="between"><span className="muted">还车里程</span><span className="mono">{v.returnKm.toLocaleString()} km</span></div>}
</>}
</div>
<div style={{marginTop:14, paddingTop:12, borderTop:"1px solid var(--border-1)", display:"flex", gap:6}}>
<button className="btn" style={{flex:1, fontSize:11}}>查看合同</button>
<button className="btn" style={{flex:1, fontSize:11}}>变更负责人</button>
</div>
</div>
</div>
{/* 氢电系统 */}
<div className="panel">
<div className="panel-head">
<Icon name="h2" size={13} style={{color:"var(--accent)"}}/>
<span className="title">氢电系统</span>
<span className="chip accent" style={{marginLeft:"auto"}}>FCEV</span>
</div>
<div style={{padding:16}}>
<div style={{display:"grid", gridTemplateColumns:"1fr 1fr", gap:14}}>
<div className="center col gap-2">
<Donut size={92} value={v.soc/100} color="var(--accent)" thick={9} label={v.soc + "%"}/>
<div className="muted" style={{fontSize:11}}>电池 SOC</div>
</div>
<div className="center col gap-2">
<Donut size={92} value={v.h2Pressure/6} color="var(--info)" thick={9} label={Math.round(v.h2Pressure/6*100) + "%"}/>
<div className="muted" style={{fontSize:11}}>H₂ 储量</div>
</div>
</div>
<div style={{marginTop:14, paddingTop:14, borderTop:"1px solid var(--border-1)", display:"grid", gridTemplateColumns:"1fr 1fr", gap:8, fontSize:11}}>
<div className="between"><span className="muted">电堆功率</span><span className="mono strong">28.4 kW</span></div>
<div className="between"><span className="muted">电池电压</span><span className="mono strong">386 V</span></div>
<div className="between"><span className="muted">电堆温度</span><span className="mono strong">76°C</span></div>
<div className="between"><span className="muted">H₂压力</span><span className="mono strong">{v.h2} MPa</span></div>
<div className="between"><span className="muted">续航估算</span><span className="mono strong" style={{color:"var(--accent)"}}>{v.range} km</span></div>
<div className="between"><span className="muted">电池温度</span><span className="mono strong">32°C</span></div>
</div>
</div>
</div>
{/* Speed/RPM curve */}
<div className="panel" style={{gridColumn:"span 2"}}>
<div className="panel-head">
<Icon name="speed" size={13}/>
<span className="title">速度 / 电机转速 · 近1小时</span>
<div className="actions">
<span className="chip">1H</span>
<span className="chip" style={{opacity:0.5}}>4H</span>
<span className="chip" style={{opacity:0.5}}>1D</span>
</div>
</div>
<div style={{padding:"14px 16px"}}>
<div className="between" style={{marginBottom:10, fontSize:11}}>
<div className="mid gap-3">
<span className="mid gap-1"><span className="dot" style={{background:"var(--info)"}}/> 速度 km/h</span>
<span className="mid gap-1"><span className="dot" style={{background:"var(--accent)"}}/> 电机RPM ÷100</span>
</div>
<div className="mono muted">avg 52 / max 89 km/h</div>
</div>
<LineChart data={genSpeed()} w={520} h={120} color="var(--info)" axis baseline={70}/>
<div style={{marginTop:-6}}>
<LineChart data={genSpeed().map(v => v*1.1)} w={520} h={60} color="var(--accent)" fill={false}/>
</div>
</div>
</div>
{/* Tire pressure */}
<div className="panel">
<div className="panel-head"><Icon name="tire" size={13}/><span className="title">胎压 / 温度</span></div>
<div style={{padding:16, display:"flex", gap:12, alignItems:"center", justifyContent:"center"}}>
<svg width="160" height="200" viewBox="0 0 160 200">
<rect x="40" y="20" width="80" height="160" rx="20" fill="var(--bg-2)" stroke="var(--border-2)"/>
<rect x="50" y="45" width="60" height="50" rx="8" fill="var(--bg-3)" opacity="0.6"/>
<rect x="50" y="105" width="60" height="50" rx="8" fill="var(--bg-3)" opacity="0.6"/>
{[
{x:24, y:38, st:"ok"},{x:120, y:38, st:"ok"},
{x:24, y:138, st:"ok"},{x:120, y:138, st:"ok"},
].map((t,i)=>(
<rect key={i} x={t.x} y={t.y} width="16" height="24" rx="3" fill="var(--ok)" opacity="0.8"/>
))}
</svg>
<div style={{display:"grid", gridTemplateColumns:"1fr 1fr", gap:6, fontSize:11, fontFamily:"var(--font-mono)"}}>
{[
{p:"FL", v:"0.24", t:"32°"},{p:"FR", v:"0.23", t:"34°"},
{p:"RL", v:"0.25", t:"36°"},{p:"RR", v:"0.24", t:"35°"},
].map((t,i)=>(
<div key={i} style={{padding:"6px 8px", background:"var(--bg-2)", borderRadius:4, border:"1px solid var(--border-1)"}}>
<div className="muted" style={{fontSize:9}}>{t.p}</div>
<div className="strong">{t.v}</div>
<div className="muted" style={{fontSize:9}}>{t.t}</div>
</div>
))}
</div>
</div>
</div>
{/* 保养与维护 */}
<div className="panel" style={{gridColumn:"span 2"}}>
<div className="panel-head">
<Icon name="wrench" size={13}/>
<span className="title">保养与维护</span>
<span className="actions">
<span className={"chip " + (v.kmToMaint < 1000 ? "warn" : "ok")} style={{fontSize:10}}>
剩余 {v.kmToMaint.toLocaleString()} km
</span>
</span>
</div>
<div style={{padding:"14px 16px"}}>
<div className="between" style={{fontSize:11, marginBottom:8}}>
<span className="muted">保养周期 10,000 km</span>
<span className="mono">已行 {(10000 - v.kmToMaint).toLocaleString()} / 10,000 km</span>
</div>
<div className="bar" style={{height:6, marginBottom:14}}>
<i style={{width: ((10000 - v.kmToMaint) / 10000 * 100) + "%", background: v.kmToMaint < 1000 ? "var(--warn)" : "var(--accent)"}}/>
</div>
<div style={{display:"grid", gridTemplateColumns:"1fr 1fr", gap:14, fontSize:11}}>
<div>
<div className="eyebrow" style={{marginBottom:8}}>上次保养</div>
<div className="col gap-1">
<div className="between"><span className="muted">日期</span><span className="strong">{v.lastMaintDays}</span></div>
<div className="between"><span className="muted">里程</span><span className="mono">{v.lastMaintKm.toLocaleString()} km</span></div>
<div className="between"><span className="muted">项目</span><span>·</span></div>
<div className="between"><span className="muted">技师</span><span></span></div>
</div>
</div>
<div>
<div className="eyebrow" style={{marginBottom:8}}>下次保养预约</div>
<div className="col gap-1">
<div className="between"><span className="muted">里程节点</span><span className="mono">{v.nextMaintKm.toLocaleString()} km</span></div>
<div className="between"><span className="muted">距离</span><span className="strong" style={{color: v.kmToMaint < 1000 ? "var(--warn)" : "var(--fg-0)"}}>{v.kmToMaint.toLocaleString()} km</span></div>
<div className="between"><span className="muted">推荐站点</span><span> · </span></div>
<div className="between"><span className="muted">通知</span><span className="strong">{v.deptLead} · {v.deptName}</span></div>
</div>
</div>
</div>
</div>
</div>
{/* DTC list */}
<div className="panel">
<div className="panel-head"><Icon name="wrench" size={13}/><span className="title">故障码 · DTC</span><span className="chip" style={{marginLeft:"auto"}}>{v.asset === "abnormal" ? "2 active" : "0 active"}</span></div>
<div style={{padding:0}}>
{(v.asset === "abnormal" ? [
{c:"P0A7F", n:"电池组性能下降", st:"warn", t:"3小时前"},
{c:"U0073", n:"控制模块通信总线A关闭", st:"warn", t:"2天前"},
{c:"P0563", n:"系统电压高", st:"info", t:"已清除"},
] : [
{c:"P0563", n:"系统电压高", st:"info", t:"已清除"},
]).map((d,i,arr)=>(
<div key={i} className="between" style={{padding:"10px 14px", borderBottom: i < arr.length-1 ? "1px solid var(--border-1)" : "none", fontSize:12}}>
<div>
<span className="mono strong">{d.c}</span>
<span className="muted" style={{marginLeft:8, fontSize:11}}>{d.n}</span>
</div>
<div className="mid gap-2">
<span className={"chip " + d.st}>{d.st === "warn" ? "ACTIVE" : "CLEAR"}</span>
<span className="muted mono" style={{fontSize:10}}>{d.t}</span>
</div>
</div>
))}
</div>
</div>
{/* Data source / signal channels */}
<div className="panel" style={{gridColumn:"1 / -1"}}>
<div className="panel-head">
<Icon name="wifi" size={13}/>
<span className="title">数据源 · 信号通道</span>
<div className="actions">
<span className="chip ok"><span className="dot ok" style={{width:5,height:5}}/> 双源在线</span>
<span className="chip">最近上行 · 218ms</span>
</div>
</div>
<div style={{padding:14, display:"grid", gridTemplateColumns:"1fr 1fr 1fr", gap:12}}>
{[
{
src:"T", title:"TBOX · 整车遥信",
spec:"GB/T 32960-2016 / GB/T 40432",
sub:"国标新能源车数据", up:"10 s",
signals:[
{n:"整车状态", c:"54 项", st:"ok"},
{n:"驱动电机", c:"18 项", st:"ok"},
{n:"动力电池", c:"32 项", st:"ok"},
{n:"燃料电池/H₂", c:"24 项", st:"ok"},
{n:"极值/故障", c:"12 项", st:"warn"},
],
health: 99.6,
},
{
src:"J", title:"JT/T 808 · 北斗位置",
spec:"JT/T 808-2019 部标",
sub:"位置/报警/参数", up:"30 s",
signals:[
{n:"GNSS位置", c:"1 帧", st:"ok"},
{n:"行驶记录仪", c:"8 项", st:"ok"},
{n:"报警/事件", c:"64 类", st:"ok"},
{n:"参数下发", c:"42 项", st:"ok"},
{n:"电子围栏", c:"6 区域", st:"ok"},
],
health: 100,
},
{
src:"J", title:"JT/T 1078 · 视频",
spec:"JT/T 1078-2016 部标",
sub:"4路实时音视频", up:"H.264",
signals:[
{n:"CH1 前向", c:"720p", st:"ok"},
{n:"CH2 驾驶员", c:"720p", st:"ok"},
{n:"CH3 后视", c:"720p", st:"ok"},
{n:"CH4 车厢", c:"480p", st:"warn"},
{n:"录像存储", c:"1.2 TB", st:"ok"},
],
health: 92.8,
},
].map((s,i)=>(
<div key={i} style={{padding:14, background:"var(--bg-2)", borderRadius:6, border:"1px solid var(--border-1)"}}>
<div className="between">
<div className="mid gap-2">
<SourceBadge src={s.src} size="md"/>
<span className="strong" style={{fontSize:12}}>{s.title}</span>
</div>
<span className="mono muted" style={{fontSize:10}}>{s.up}</span>
</div>
<div className="muted mono" style={{fontSize:10, marginTop:4}}>{s.spec}</div>
<div className="muted" style={{fontSize:11, marginTop:2}}>{s.sub}</div>
<div style={{marginTop:10, display:"flex", flexDirection:"column", gap:4}}>
{s.signals.map((sig,j)=>(
<div key={j} className="between" style={{fontSize:11, padding:"4px 0"}}>
<span className="mid gap-2"><span className={"dot " + sig.st} style={{width:5,height:5}}/><span className="muted">{sig.n}</span></span>
<span className="mono">{sig.c}</span>
</div>
))}
</div>
<div style={{marginTop:10, paddingTop:10, borderTop:"1px solid var(--border-1)"}}>
<div className="between" style={{fontSize:10}}>
<span className="muted">通道完好率</span>
<span className="mono strong" style={{color: s.health > 99 ? "var(--ok)" : s.health > 95 ? "var(--info)" : "var(--warn)"}}>{s.health}%</span>
</div>
<div style={{height:3, background:"var(--bg-3)", borderRadius:2, marginTop:4, overflow:"hidden"}}>
<div style={{height:"100%", width: s.health + "%", background: s.health > 99 ? "var(--ok)" : s.health > 95 ? "var(--info)" : "var(--warn)"}}/>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
);
};
window.ArtboardDetail = ArtboardDetail;