Files
ln-bi/src/modules/assets/faults/DashboardPanels.tsx
T
2026-10-16 18:15:44 +08:00

43 lines
10 KiB
TypeScript
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.
import { useState } from 'react';
import { Bar, BarChart, CartesianGrid, LabelList, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
import { ArrowUpRight, Info } from 'lucide-react';
import type { FaultAnalysisRecord, FaultDashboardDimension, FaultDashboardResponse, FaultDashboardScope, FaultDistribution } from '../../../shared/assets/faults';
export type DashboardDrill = (title: string, scope?: FaultDashboardScope, filters?: Record<string, string>) => void;
const colors = ['#ef4444','#f59e0b','#2563eb','#93c5fd','#94a3b8'];
export function DashboardPanels({data, drill}:{data:FaultDashboardResponse;drill:DashboardDrill}) {
const [dimension,setDimension]=useState<'region'|'level'|'customer'|'model'>('region');
const [metric,setMetric]=useState<'reported'|'vehicles'>('reported');
const [expanded,setExpanded]=useState(false),[matrixExpanded,setMatrixExpanded]=useState(false),[customersExpanded,setCustomersExpanded]=useState(false);
const groups=data.distributions[dimension],shown=expanded?groups:groups.slice(0,8);
const levels=[...data.distributions.level].sort((a,b)=>(a.value==='未标注'?1:0)-(b.value==='未标注'?1:0)||a.value.localeCompare(b.value,'zh-CN'));
const models=matrixExpanded?data.matrix.models:data.matrix.models.slice(0,3);
const max=Math.max(1,...data.matrix.cells.map(c=>c.count));
const customers=[...data.distributions.customer].sort((a,b)=>b.vehicles-a.vehicles||b.reported-a.reported);
const chartDrill=(datum:unknown,key:FaultDashboardDimension,title:string)=>{const row=(datum as {payload?:FaultDistribution})?.payload;if(row)drill(`${title} · ${row.label}`,'cohort',{[key]:row.value});};
return <>
<div className="fd-grid fd-grid-primary">
<section className="fd-panel" aria-label="故障分布与对比"><div className="fd-panel-title"><h2>故障分布与对比</h2><span>按本期提报队列统计,截至当前的闭环状态。</span></div>
<div className="fd-chart-controls"><div className="fd-segment" aria-label="故障分布维度">{([['region','区域'],['level','程度'],['customer','客户'],['model','车型']] as const).map(([key,label])=><button key={key} aria-pressed={dimension===key} onClick={()=>{setDimension(key);setExpanded(false);}}>{label}</button>)}</div><div className="fd-segment" aria-label="故障分布指标"><button aria-pressed={metric==='reported'} onClick={()=>setMetric('reported')}>故障量</button><button aria-pressed={metric==='vehicles'} onClick={()=>setMetric('vehicles')}>受影响车辆</button><button disabled title="缺少同期服务车辆规模,暂不计算发生频率">发生频率 · 待补分母</button></div></div>
<div className="fd-legend">{metric==='reported'?<><span className="fd-blue">已闭环 {groups.reduce((n,r)=>n+r.closed,0)}</span><span className="fd-orange">未闭环 {groups.reduce((n,r)=>n+r.unclosed,0)}</span></>:<span className="fd-blue">按车辆去重 · 辆</span>}</div>
{shown.length?<div className="fd-distribution-chart"><ResponsiveContainer width="100%" height={Math.max(150,shown.length*26)} minWidth={0}><BarChart data={shown} layout="vertical" margin={{top:4,right:30,left:0,bottom:0}}>
<CartesianGrid horizontal={false} stroke="#edf2fa"/><XAxis type="number" allowDecimals={false} axisLine={false} tickLine={false} tick={{fontSize:10,fill:'#7c8ba7'}}/><YAxis type="category" dataKey="label" interval={0} width={116} axisLine={false} tickLine={false} tick={{fontSize:11,fill:'#334155'}} tickFormatter={v=>v.length>12?`${v.slice(0,11)}…`:v}/><Tooltip cursor={{fill:'#f4f7fc'}} contentStyle={{borderRadius:8,borderColor:'#e2e8f0',fontSize:12}}/>
{metric==='reported'?<><Bar dataKey="closed" name="已闭环" stackId="faults" fill="#2475ff" maxBarSize={18} isAnimationActive={false} cursor="pointer" onClick={d=>chartDrill(d,dimension,'故障分布')}><LabelList dataKey={(row:FaultDistribution)=>row.unclosed===0?row.reported:''} position="right" fontSize={10} fill="#334155"/></Bar><Bar dataKey="unclosed" name="未闭环" stackId="faults" fill="#ff981a" maxBarSize={18} isAnimationActive={false} cursor="pointer" onClick={d=>chartDrill(d,dimension,'故障分布')}><LabelList dataKey="reported" position="right" fontSize={10} fill="#334155"/></Bar></>:<Bar dataKey="vehicles" name="受影响车辆" fill="#2475ff" maxBarSize={18} isAnimationActive={false} cursor="pointer" onClick={d=>chartDrill(d,dimension,'受影响车辆对应故障')}><LabelList dataKey="vehicles" position="right" fontSize={10} fill="#334155"/></Bar>}</BarChart></ResponsiveContainer></div>:<p className="fd-empty">所选条件下暂无故障。</p>}
<div className="fd-panel-foot"><span>点击图表查看对应故障;车辆跨组可能重复,不直接累加。</span>{groups.length>8&&<button onClick={()=>setExpanded(!expanded)}>{expanded?'收起':`全部 ${groups.length} 组`}</button>}</div>
<details className="fd-chart-data"><summary>查看精确数量与下钻</summary><table><thead><tr><th>分组</th><th>故障量</th><th>已闭环</th><th>未闭环</th><th>车辆</th></tr></thead><tbody>{groups.map(r=><tr key={r.value}><td><button onClick={()=>drill(r.label,'cohort',{[dimension]:r.value})}>{r.label}</button></td><td>{r.reported}</td><td>{r.closed}</td><td>{r.unclosed}</td><td>{r.vehicles}</td></tr>)}</tbody></table></details>
</section>
<section className="fd-panel" aria-label="故障程度"><div className="fd-panel-title"><h2>故障程度</h2><span className="fd-chip">现有等级口径</span></div><p className="fd-caption">按本期故障数量统计,点击等级查看明细。</p>
<div className="fd-level-list">{levels.map(row=><button key={row.value} onClick={()=>drill(`故障程度 · ${row.label}`,'cohort',{level:row.value})}><span>{row.label}</span><span className="fd-level-track"><span style={{width:`${row.reported/Math.max(1,...levels.map(r=>r.reported))*100}%`,background:colors[/^L[1-4]/.test(row.value)?Number(row.value[1])-1:4]}}/></span><b>{row.reported}</b><ArrowUpRight size={12}/></button>)}</div>
{!levels.length&&<p className="fd-empty">暂无等级统计。</p>}
<p className="fd-info"><Info size={15}/>等级不等于实际停运影响,请结合车辆状态与服务记录判断。</p><p className="fd-caption">未标注等级 {data.quality.missingLevel} 单;停运车辆与修复时长待补数据。</p>
</section>
</div>
<div className="fd-grid fd-grid-secondary">
<section className="fd-panel" aria-label="品牌车型与故障部位"><div className="fd-panel-title"><h2>品牌车型 × 故障部位</h2><span>点击交叉单元格查看对应故障。</span></div><div className="fd-table-scroll"><table className="fd-matrix"><thead><tr><th>品牌车型</th>{data.matrix.categories.map(c=><th key={c.value}>{c.label}</th>)}<th>合计</th></tr></thead><tbody>{models.map(m=><tr key={m.value}><th title={m.label}>{m.label}</th>{data.matrix.categories.map(c=>{const count=data.matrix.cells.find(cell=>cell.model===m.value&&cell.category===c.value)?.count||0;return <td key={c.value} style={{background:count?`rgba(36,117,255,${.08+.52*count/max})`:undefined}}><button disabled={!count} aria-label={`${m.label},${c.label},${count} 单`} onClick={()=>drill(`${m.label} · ${c.label}`,'cohort',{model:m.value,category:c.value})}>{count||'—'}</button></td>})}<td><button onClick={()=>drill(m.label,'cohort',{model:m.value})}>{data.matrix.totals[m.value]}</button></td></tr>)}</tbody></table></div>{!models.length&&<p className="fd-empty">暂无车型与部位数据。</p>}<div className="fd-panel-foot"><span>一单多部位分别计入,合计按故障单去重。</span>{data.matrix.models.length>3&&<button onClick={()=>setMatrixExpanded(!matrixExpanded)}>{matrixExpanded?'收起':`全部 ${data.matrix.models.length} 个车型`}</button>}</div></section>
<section className="fd-panel" aria-label="客户受影响情况"><div className="fd-panel-title"><h2>客户受影响情况</h2><span>同一车辆在同一客户下去重。</span></div><div className="fd-table-scroll"><table className="fd-customers"><thead><tr><th>#</th><th>客户名称</th><th>本期影响车辆</th><th>当前在办</th></tr></thead><tbody>{(customersExpanded?customers:customers.slice(0,4)).map((r,i)=><tr key={r.value}><td>{i+1}</td><td><button onClick={()=>drill(r.label,'cohort',{customer:r.value})}>{r.label}</button></td><td><button className="fd-customer-meter" onClick={()=>drill(`${r.label} · 本期故障`,'cohort',{customer:r.value})}><progress max={Math.max(1,customers[0]?.vehicles||0)} value={r.vehicles}/><b>{r.vehicles}</b></button></td><td><button className="fd-danger" disabled={!r.open} onClick={()=>drill(`${r.label} · 当前在办`,'open',{customer:r.value})}>{r.open}</button></td></tr>)}</tbody></table></div>{!customers.length&&<p className="fd-empty">暂无客户影响数据。</p>}<div className="fd-panel-foot"><span>当前在办不限制提报日期;库存车单独展示。</span>{customers.length>4&&<button onClick={()=>setCustomersExpanded(!customersExpanded)}>{customersExpanded?'收起':`全部 ${customers.length} 组`}</button>}</div></section>
</div>
</>;
}
export function DashboardTicketTable({rows,updatedAt,drill,emptyMessage='当前条件下暂无故障。'}:{rows:FaultAnalysisRecord[];updatedAt:string;drill:DashboardDrill;emptyMessage?:string}) {
return <div className="fd-table-scroll"><table className="fd-tickets"><thead><tr><th>车辆 / 故障单号</th><th>运维处置人</th><th>区域 / 客户</th><th>等级</th><th>当前状态</th><th>处置记录 / 故障描述</th><th>提报时间</th><th>操作</th></tr></thead><tbody>{rows.map(row=>{const overdue=['待处理','处理中'].includes(row.status)&&row.deadlineAt&&Date.parse(`${row.deadlineAt.replace(' ','T')}+08:00`)<Date.parse(updatedAt);return <tr key={row.id}><td><strong>{row.plate}</strong><small>{row.code}</small></td><td>{row.handlers.map(h=>h.label).join('、')}</td><td>{row.cell.region}<small>{row.customerLabel}</small></td><td><span className={`fd-grade fd-grade-${row.level?.slice(0,2)}`}>{row.level||'未标注'}</span></td><td><span className={row.status==='已归档'?'fd-green':'fd-orange'}>{row.status==='已归档'?'已闭环':row.status}</span>{overdue&&<small className="fd-overdue-tag">已逾期</small>}</td><td className="fd-description" title={row.progress||row.description}>{row.progress||row.description||'暂无处置记录'}</td><td>{row.reportedAt||'未记录'}</td><td><button onClick={()=>drill(`${row.plate} · ${row.code}`,'ledger',{id:row.id})}>查看</button></td></tr>})}</tbody></table>{!rows.length&&<p className="fd-empty">{emptyMessage}</p>}</div>;
}