70 lines
5.3 KiB
TypeScript
70 lines
5.3 KiB
TypeScript
import type { ReactNode } from 'react';
|
||
import { Fragment, useCallback, useEffect, useState } from 'react';
|
||
import { ArrowUpRight, CheckCircle2, ClipboardList, Clock3, PlusCircle, RefreshCw } from 'lucide-react';
|
||
import { fetchJson } from '../../../auth/api-client';
|
||
import { FAULT_OVERVIEW_METRICS, type FaultOverview, type FaultOverviewMetric } from '../../../shared/assets/faults';
|
||
import { FaultAnalysisModal } from './FaultAnalysisModal';
|
||
import { FaultWeeklyReport } from './FaultWeeklyReport';
|
||
import { FaultTrendCard } from './FaultTrendCard';
|
||
import './fault-management.css';
|
||
|
||
export const FAULT_DEFINITIONS = {
|
||
reported: '按今日提报时间统计', closed: '按今日解决时间统计',
|
||
open: '当前在办 = 待处理 + 处理中', ledger: '全部有效故障台账',
|
||
};
|
||
const icons = { reported: PlusCircle, closed: CheckCircle2, open: Clock3, ledger: ClipboardList };
|
||
const metrics = ['reported', 'closed', 'open', 'ledger'] as const;
|
||
export function FaultOverview({ navigation }: { navigation: ReactNode }) {
|
||
const [data, setData] = useState<FaultOverview | null>(null);
|
||
const [error, setError] = useState('');
|
||
const [loading, setLoading] = useState(true);
|
||
const [revision, setRevision] = useState(0);
|
||
const [metric, setMetric] = useState<FaultOverviewMetric | null>(null);
|
||
const close = useCallback(() => setMetric(null), []);
|
||
useEffect(() => {
|
||
const controller = new AbortController();
|
||
setLoading(true); setError('');
|
||
fetchJson<FaultOverview>('/api/asset-faults/overview', { signal: controller.signal })
|
||
.then(result => { if (!controller.signal.aborted) setData(result); }).catch(e => { if (!controller.signal.aborted) setError(e instanceof Error ? e.message : '加载失败'); })
|
||
.finally(() => { if (!controller.signal.aborted) setLoading(false); });
|
||
return () => controller.abort();
|
||
}, [revision]);
|
||
return <div className="fault-page fault-overview-page">
|
||
<header className="fd-header">
|
||
<div><p className="fd-breadcrumb">羚牛氢能 BI / 故障分析</p><div className="fd-heading-line"><h1>羚牛氢能-故障管理</h1><p>实时掌握故障数量、闭环趋势与运维进展。</p></div></div>
|
||
<div className="fd-header-actions"><span>{data ? `更新时间:${new Date(data.updatedAt).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false })}` : '正在读取故障台账'}</span><button aria-label="刷新故障统计" disabled={loading} onClick={() => setRevision(v => v + 1)}><RefreshCw size={14} className={loading ? 'animate-spin' : ''} />刷新</button></div>
|
||
</header>
|
||
{navigation}
|
||
{error && <div className="fault-message" role="alert">{error}<button onClick={() => setRevision(v => v + 1)}>重试</button></div>}
|
||
<section className="fault-summary" aria-label="故障总统计" aria-busy={loading}>
|
||
<div className="fault-summary-heading"><h2>故障数据实时汇总</h2><span>{data?.date || '—'} · 北京时间</span></div>
|
||
<div className="fault-metrics">{metrics.map(key => {
|
||
const Icon = icons[key];
|
||
return <article key={key} className={`fault-metric fault-metric-${key}`}><button className="fault-metric-main" disabled={!data || loading || !!error} onClick={() => setMetric(key)} aria-label={`${FAULT_OVERVIEW_METRICS[key]} ${data?.totals[key] ?? '加载中'} 单,查看分析`}>
|
||
<div className="fault-metric-label"><span className="fault-metric-icon"><Icon size={19} /></span><span>{FAULT_OVERVIEW_METRICS[key]}</span><ArrowUpRight size={15} /></div>
|
||
<div className="fault-metric-count"><strong>{loading ? '—' : data?.totals[key].toLocaleString('zh-CN') ?? '—'}</strong><span>单</span></div>
|
||
<p>{FAULT_DEFINITIONS[key]}</p><span className="fault-metric-link">查看分析与明细 →</span>
|
||
</button>
|
||
{key === 'open' && <>
|
||
<button className="fault-overdue-subset" disabled={!data || loading || !!error} onClick={() => setMetric('overdue')} aria-label={`逾期 ${data?.totals.overdue ?? '加载中'} 单,查看分析`}>
|
||
<span>逾期</span><strong>{loading ? '—' : data?.totals.overdue?.toLocaleString('zh-CN') ?? '—'}</strong><small>单</small>
|
||
</button>
|
||
<div className="fault-open-breakdown" aria-label="当前在办等于待处理加处理中">
|
||
{(['waiting', 'processing'] as const).map((part, index) => <Fragment key={part}>
|
||
{index > 0 && <span className="fault-composition-plus" aria-hidden="true">+</span>}
|
||
<button className={`fault-submetric fault-submetric-${part}`} disabled={!data || loading || !!error} onClick={() => setMetric(part)} aria-label={`${FAULT_OVERVIEW_METRICS[part]} ${data?.totals[part] ?? '加载中'} 单,查看分析`}>
|
||
<span>{FAULT_OVERVIEW_METRICS[part]}</span><strong>{loading ? '—' : data?.totals[part]?.toLocaleString('zh-CN') ?? '—'}</strong>
|
||
</button>
|
||
</Fragment>)}
|
||
</div>
|
||
</>}
|
||
</article>;
|
||
})}</div>
|
||
<p className="fault-summary-note">当前在办 = 待处理 + 处理中,逾期为在办子集,不重复累加;按处置截止时间统计,挂起停表,未设置截止时间不计入逾期。</p>
|
||
</section>
|
||
<FaultTrendCard refresh={revision} />
|
||
<FaultWeeklyReport refresh={revision} />
|
||
{metric && <FaultAnalysisModal key={metric} metric={metric} onClose={close} />}
|
||
</div>;
|
||
}
|