@@ -24,13 +24,14 @@ export default function FaultManagement(){
const query = new URLSearchParams ( { start :applied.start , end :applied.end , . . . applied . filters } ) . toString ( ) ;
useEffect ( ( ) = > { if ( tab === '管理总览' ) return ; const c = new AbortController ( ) ; setLoading ( true ) ; setError ( '' ) ; fetchJson < FaultDashboardResponse > ( ` /api/asset-faults/dashboard? ${ query } ` , { signal :c.signal } ) . then ( d = > { if ( ! c . signal . aborted ) setData ( d ) ; } ) . catch ( e = > { if ( ! c . signal . aborted ) setError ( e instanceof Error ? e . message : '加载失败' ) ; } ) . finally ( ( ) = > { if ( ! c . signal . aborted ) setLoading ( false ) ; } ) ; return ( ) = > c . abort ( ) ; } , [ query , revision , tab ] ) ;
useEffect ( ( ) = > { if ( tab !== '故障明细' ) return ; const c = new AbortController ( ) ; setDetailData ( null ) ; setDetailError ( '' ) ; setDetailLoading ( true ) ; setLimit ( 50 ) ; fetchJson < FaultAnalysisResponse > ( ` /api/asset-faults/dashboard-analysis? ${ query } &scope= ${ detailScope } ` , { signal :c.signal } ) . then ( d = > { if ( ! c . signal . aborted ) setDetailData ( d ) ; } ) . catch ( ( ) = > { if ( ! c . signal . aborted ) setDetailError ( '故障明细加载失败,请重试。' ) ; } ) . finally ( ( ) = > { if ( ! c . signal . aborted ) setDetailLoading ( false ) ; } ) ; return ( ) = > c . abort ( ) ; } , [ tab , query , detailScope , revision ] ) ;
const drill :DashboardDrill = ( title , scope = 'cohort' , extra = { } ) = > { if ( loading || error || ! data ) return ; const params = new URLSearchParams ( { start :data.start , end :data.end , . . . applied . filters , . . . extra , scope } ) ; setSource ( { url : ` /api/asset-faults/dashboard-analysis? ${ params } ` , title , periodLabel :scope === 'cohort' ? ` ${ data . start } 至 ${ data . end } · 按提报日期 ` : scope === 'reported' || scope === 'closed' ? '北京时间今日' : '全部提报日期 · 当前状态' , defaultDimensions :extra.id? [ ] : [ 'handler' , 'faultStatus' ] , initialDetails : ! ! extra . id , definition :scope === 'cohort' ? '按提报日期筛选同批故障,已归档为已闭环,其余状态为未闭环。' : scope === 'overdue' ? '逾期仅包含待处理、处理中且处置截止时间早于数据时间的故障;挂起、未设置截止时间不计入。' : scope === 'reported' ? '按北京时间今日提报时间统计。' : scope === 'closed' ? '按北京时间今日解决时间统计,不限制提报日期。' : '不限制提报日期,按当前状态及已应用条件统计;当前在办 = 待处理 + 处理中。' } ) ; } ;
const drill :DashboardDrill = ( title , scope = 'cohort' , extra = { } ) = > { if ( loading || error || ! data ) return ; const params = new URLSearchParams ( { start :data.start , end :data.end , . . . applied . filters , . . . extra , scope } ) ; setSource ( { url : ` /api/asset-faults/dashboard-analysis? ${ params } ` , title , periodLabel :scope === 'cohort' ? ` ${ data . start } 至 ${ data . end } · 按提报日期 ` : scope === 'reported' || scope === 'closed' ? '北京时间今日' : '全部提报日期 · 当前状态' , defaultDimensions :extra.id? [ ] : [ 'handler' , 'faultStatus' ] , initialDetails : ! ! extra . id , definition :scope === 'focus' ? '仅包含 L1-特急且未归档的故障(含待处理、处理中、挂起等状态),不限制提报日期。' : scope === ' cohort'? '按提报日期筛选同批故障,已归档为已闭环,其余状态为未闭环。' : scope === 'overdue' ? '逾期仅包含待处理、处理中且处置截止时间早于数据时间的故障;挂起、未设置截止时间不计入。' : scope === 'reported' ? '按北京时间今日提报时间统计。' : scope === 'closed' ? '按北京时间今日解决时间统计,不限制提报日期。' : '不限制提报日期,按当前状态及已应用条件统计;当前在办 = 待处理 + 处理中。' } ) ; } ;
const t = data ? . totals ;
const detailRows = ( detailData ? . rows || [ ] ) . filter ( r = > ` ${ r . code } ${ r . plate } ${ r . description } ${ r . handlers . map ( h = > h . label ) . join ( ' ' ) } ` . toLowerCase ( ) . includes ( search . trim ( ) . toLowerCase ( ) ) ) . sort ( ( a , b ) = > ( b . reportedAt || '' ) . localeCompare ( a . reportedAt || '' ) || b . id . localeCompare ( a . id , 'en' , { numeric :true } ) ) ;
const dirty = range . start !== applied . start || range . end !== applied . end || JSON . stringify ( filters ) !== JSON . stringify ( applied . filters ) ;
const count = ( n? :number ) = > loading ? '—' : n ? . toLocaleString ( 'zh-CN' ) ? ? '—' ;
const select = ( key :FaultDashboardDimension ) = > < label className = "fd-filter" key = { key } > < span > { FAULT_DASHBOARD_FILTERS [ key ] } < / span > < select aria-label = { FAULT_DASHBOARD_FILTERS [ key ] } value = { filters [ key ] || '' } onChange = { e = > setFilters ( v = > ( { . . . v , [ key ] : e . target . value } ) ) } > < option value = "" > 全 部 < / option > { data ? . options [ key ] . map ( o = > < option key = { o . value } value = { o . value } > { o . label } < / option > ) } < / select > < / label > ;
const showAllOpen = ( ) = > { setDetailScope ( 'open' ) ; setTab ( '故障明细' ) ; setSearch ( '' ) ; } ;
const showAllFaults = ( ) = > { setFilters ( { } ) ; setApplied ( { . . . range , filters : { } } ) ; setDetailScope ( 'ledger' ) ; setTab ( '故障明细' ) ; setSearch ( '' ) ; } ;
const navigation = < nav className = "fd-tabs" aria-label = "故障分析页面" > { tabs . map ( name = > < button key = { name } aria-current = { tab === name ? 'page' : undefined } onClick = { ( ) = > setTab ( name ) } > { name } < / button > ) } < / nav > ;
if ( tab === '管理总览' ) return < FaultOverview navigation = { navigation } / > ;
return < div className = "fault-page fd-page" >
@@ -41,12 +42,11 @@ export default function FaultManagement(){
{ error && < div className = "fault-message" role = "alert" > { error } < button onClick = { ( ) = > setRevision ( v = > v + 1 ) } > 重 试 < / button > < / div > }
{ ! data && loading ? < div className = "fd-loading" role = "status" > < RefreshCw className = "animate-spin" size = { 20 } / > 正 在 汇 总 故 障 分 析 … < / div > : data && < div className = { loading || error ? 'fd-content is-loading' : 'fd-content' } aria-busy = { loading } >
{ tab === '多维分析' && < >
< section className = "fd-kpis fd-kpis-five " aria-label = "故障总统计" >
< article className = "fd-kpi" > < button disabled = { loading || ! ! error } onClick = { ( ) = > drill ( '期间新增故障' ) } > < ClipboardList className = "fd-kpi-icon" / > < span > 期 间 新 增 故 障 < / span > < ArrowUpRight size = { 14 } / > < strong > { count ( t ? . reported ) } < small > 单 < / small > < / strong > < / button > < p > 对 比 上 期 < b className = { t ? . change != null && t . change > 0 ? 'fd-orange' : 'fd-blue' } > { t ? . change == null ? '无可比基数' : ` ${ t . change > 0 ? '+' : '' } ${ t . change . toFixed ( 1 ) } % ` } < / b > < small > 上 期 { t ? . previous } 单 < / small > < / p > < / article >
< section className = "fd-kpis" aria-label = "故障总统计" >
< article className = "fd-kpi fd-reported-kpi " > < button disabled = { loading || ! ! error } onClick = { ( ) = > drill ( '期间新增故障' ) } > < ClipboardList className = "fd-kpi-icon" / > < span > 期 间 新 增 故 障 < / span > < ArrowUpRight size = { 14 } / > < strong > { count ( t ? . reported ) } < small > 单 < / small > < / strong > < / button > < p > 对 比 上 期 < b className = { t ? . change != null && t . change > 0 ? 'fd-orange' : 'fd-blue' } > { t ? . change == null ? '无可比基数' : ` ${ t . change > 0 ? '+' : '' } ${ t . change . toFixed ( 1 ) } % ` } < / b > < small > 上 期 { t ? . previous } 单 < / small > < / p > < div className = "fd-kpi-parts fd-report-parts" aria-label = "期间新增故障提报来源" > < button disabled = { loading || ! ! error } onClick = { ( ) = > drill ( '运维提报' , 'cohort' , { reportMethod : 'operations' } ) } > 运 维 提 报 < b > { count ( t ? . operationsReported ) } < / b > < / button > < span > + < / span > < button disabled = { loading || ! ! error } onClick = { ( ) = > drill ( 'AI提报' , 'cohort' , { reportMethod : 'customer' } ) } > AI提报 < b > { count ( t ? . customerReported ) } < / b > < / button > < / div > < / article >
< article className = "fd-kpi" > < button disabled = { loading || ! ! error } onClick = { ( ) = > drill ( '受影响车辆对应故障' ) } > < CarFront className = "fd-kpi-icon fd-purple" / > < span > 受 影 响 车 辆 < / span > < ArrowUpRight size = { 14 } / > < strong > { count ( t ? . vehicles ) } < small > 辆 < / small > < / strong > < / button > < p > 涉 及 < b > { t ? . customers } < / b > 家 已 关 联 客 户 < small > 按 车 辆 去 重 < / small > < / p > < / article >
< article className = "fd-kpi" > < button disabled = { loading || ! ! error } onClick = { ( ) = > drill ( '当前在办' , 'open' ) } > < Clock3 className = "fd-kpi-icon fd-orange" / > < span > 当 前 在 办 < / span > < ArrowUpRight size = { 14 } / > < strong > { count ( t ? . open ) } < small > 单 < / small > < / strong > < / button > < div className = "fd-kpi-parts" > < button disabled = { loading || ! ! error } onClick = { ( ) = > drill ( '待处理' , 'waiting' ) } > 待 处 理 < b > { count ( t ? . waiting ) } < / b > < / button > < span > + < / span > < button disabled = { loading || ! ! error } onClick = { ( ) = > drill ( '处理中' , 'processing' ) } > 处 理 中 < b > { count ( t ? . processing ) } < / b > < / button > < / div > < / article >
< article className = "fd-kpi" > < button disabled = { loading || ! ! error } onClick = { ( ) = > drill ( '其中 逾期' , 'overdue' ) } > < TriangleAlert className = "fd-kpi-icon fd-danger" / > < span > 其 中 逾 期 < / span > < ArrowUpRight size = { 14 } / > < strong className = "fd-danger" > { count ( t ? . overdue ) } < small > 单 < / small > < / strong > < / button > < p > 最 长 逾 期 < b className = "fd-danger" > { t ? . longestOverdueDays } 天 < / b > < small > 在 办 子 集 < / small > < / p > < / article >
< article className = "fd-kpi" > < button disabled = { loading || ! ! error } onClick = { ( ) = > drill ( '累计台账' , 'ledger' ) } > < ClipboardList className = "fd-kpi-icon fd-purple" / > < span > 累 计 台 账 < / span > < ArrowUpRight size = { 14 } / > < strong > { count ( t ? . ledger ) } < small > 单 < / small > < / strong > < / button > < p > 全 部 有 效 故 障 台 账 < small > 不 限 提 报 日 期 < / small > < / p > < / article >
< article className = "fd-kpi" > < button disabled = { loading || ! ! error } onClick = { ( ) = > drill ( '逾期' , 'overdue' ) } > < TriangleAlert className = "fd-kpi-icon fd-danger" / > < span > 逾 期 < / span > < ArrowUpRight size = { 14 } / > < strong className = "fd-danger" > { count ( t ? . overdue ) } < small > 单 < / small > < / strong > < / button > < p > 最 长 逾 期 < b className = "fd-danger" > { t ? . longestOverdueDays } 天 < / b > < small > 在 办 子 集 < / small > < / p > < / article >
< / section >
< div className = "fd-today-stats" > < span > 实 时 统 计 < / span > < button onClick = { ( ) = > drill ( '今日新增' , 'reported' ) } > 今 日 新 增 < b > { t ? . todayReported } < / b > < / button > < button onClick = { ( ) = > drill ( '本日解决' , 'closed' ) } > 本 日 解 决 < b > { t ? . todayClosed } < / b > < / button > < / div >
< div className = "fd-insight" > < Info size = { 18 } / > < strong > { data . distributions . region [ 0 ] ? ` 本期关注: ${ data . distributions . region [ 0 ] . label } 故障量最高,共 ${ data . distributions . region [ 0 ] . reported } 单;当前 ${ t ? . overdue } 单在办故障已逾期。 ` : '所选条件下暂无本期故障,可调整日期或筛选范围。' } < / strong > < span > 按 数 量 描 述 , 不 代 表 故 障 发 生 率 。 < / span > < button disabled = { ! t ? . open } onClick = { showAllOpen } > 查 看 关 联 故 障 < ArrowUpRight size = { 13 } / > < / button > < / div >
@@ -55,12 +55,12 @@ export default function FaultManagement(){
< section className = "fd-panel fd-focus" aria-label = "重点故障跟进" >
< div className = "fd-panel-title" >
< h2 > 重 点 故 障 跟 进 < / h2 >
< span className = "fd-chip fd-orange" > 当 前 在 办 { t ? . open } < / span >
< span className = "fd-chip fd-danger" > 其 中 逾 期 { t ? . overdue } < / span >
< button onClick = { showAllOpen } > 查 看 全 部 < ArrowUpRight size = { 13 } / > < / button >
< span className = "fd-chip fd-orange" > L1 未 闭 环 { data . focusTotals . total } < / span >
< span className = "fd-chip fd-danger" > 逾 期 { data . focusTotals . overdue } < / span >
< button onClick = { showAllFaults } title = "查看当前权限内的全部故障记录" > 查 看 全 部 < ArrowUpRight size = { 13 } / > < / button >
< / div >
< DashboardTicketTable rows = { data . focus } updatedAt = { data . updatedAt } drill = { drill } / >
< p className = "fd-caption" > 沿 用 当 前 业 务 筛 选 , 不 限 制 提 报 日 期 ; 优 先 展 示 逾 期 故 障 , 同 优 先 级 按 提 报 时 间 倒 序 , 最 多 展 示 5 单 。 完 整 明 细 见 “ 查 看 全 部 ” 。 < / p >
< DashboardTicketTable rows = { data . focus } updatedAt = { data . updatedAt } drill = { drill } emptyMessage = "当前筛选范围内暂无 L1 未闭环故障。" / >
< p className = "fd-caption" > 仅 展 示 L1 - 特 急 且 未 闭 环 ( 未 归 档 ) 的 故 障 ; 沿 用 当 前 业 务 筛 选 , 不 限 制 提 报 日 期 。 逾 期 优 先 , 同 优 先 级 按 提 报 时 间 倒 序 , 最 多 展 示 5 单 。 “ 查 看 全 部 ” 进 入 当 前 权 限 内 的 全 部 故 障 台 账 , 并 清 除 业 务 筛 选 和 搜 索 条 件 , 不 限 等 级 、 状 态 及 提 报 日 期 。 < / p >
< / section >
< / > }
{ tab === '故障明细' && < section className = "fd-panel fd-details" > < div className = "fd-panel-title" > < h2 > 故 障 明 细 < / h2 > < span > 按 提 报 时 间 倒 序 · { detailRows . length } 单 < / span > < button onClick = { ( ) = > drill ( '故障明细多维分析' , detailScope ) } > 多 维 分 析 < ArrowUpRight size = { 13 } / > < / button > < / div > < div className = "fd-detail-controls" > < div className = "fd-segment" > { ( [ [ 'cohort' , '本期提报' ] , [ 'open' , '当前在办' ] , [ 'overdue' , '逾期故障' ] , [ 'ledger' , '累计台账' ] ] as const ) . map ( ( [ key , title ] ) = > < button key = { key } aria-pressed = { detailScope === key } onClick = { ( ) = > { setDetailScope ( key ) ; setSearch ( '' ) ; } } > { title } < / button > ) } < / div > < label > < Search size = { 15 } / > < input placeholder = "搜索车牌、单号、处置人" aria-label = "搜索故障明细列表" value = { search } onChange = { e = > { setSearch ( e . target . value ) ; setLimit ( 50 ) ; } } / > < / label > < / div > { detailLoading ? < p className = "fd-loading" role = "status" > 正 在 加 载 故 障 明 细 … < / p > : detailError ? < p role = "alert" className = "fault-message" > { detailError } < button onClick = { ( ) = > setRevision ( v = > v + 1 ) } > 重 试 < / button > < / p > : < > < DashboardTicketTable rows = { detailRows . slice ( 0 , limit ) } updatedAt = { detailData ? . updatedAt || data . updatedAt } drill = { drill } / > { detailRows . length > limit && < button className = "fd-load-more" onClick = { ( ) = > setLimit ( v = > v + 50 ) } > 加 载 更 多 ( { Math . min ( limit , detailRows . length ) } / { detailRows . length } ) < / button > } < / > } < / section > }