import type { FaultDashboardFilters } from '../../../shared/assets/faults'; import { useCallback, useEffect, useRef, useState } from 'react'; import { CalendarDays, ChevronDown, RefreshCw } from 'lucide-react'; import { Bar, BarChart, CartesianGrid, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; import { fetchJson } from '../../../auth/api-client'; import type { FaultTrendResponse, FaultTrendStatus } from '../../../shared/assets/faults'; import { AnalysisExportButton } from '../components/analysis/AnalysisExportButton'; import { downloadFaultReport } from './report-export'; import { FlowDateRangePicker } from '../components/overview/FlowDateRangePicker'; import { FaultAnalysisModal, type FaultAnalysisSource } from './FaultAnalysisModal'; function initialRange() { const end = new Date(Date.now() + 8 * 3600000).toISOString().slice(0, 10); return { start: new Date(Date.parse(end) - 29 * 86400000).toISOString().slice(0, 10), end }; } const statusLabels = { all: '新增故障', closed: '其中已闭环', unclosed: '其中未闭环' }; const countKeys = { all: 'reported', closed: 'closed', unclosed: 'unclosed' } as const; export function FaultTrendCard({ refresh, scope = {}, scopeLabel = '', period }: { refresh: number; scope?: FaultDashboardFilters; scopeLabel?: string; period?: { start: string; end: string } }) { const [range, setRange] = useState(() => period || initialRange()); const [granularity, setGranularity] = useState<'day' | 'month'>('day'); const scopeKey = new URLSearchParams(scope).toString(); useEffect(() => { if (period) setRange(period); }, [period?.start, period?.end]); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [revision, setRevision] = useState(0); const [expanded, setExpanded] = useState(false); const [source, setSource] = useState(null); const chartRef = useRef(null); const close = useCallback(() => setSource(null), []); useEffect(() => { const controller = new AbortController(); setLoading(true); setError(''); setData(null); const params = new URLSearchParams({ ...range, granularity, ...scope }); fetchJson(`/api/asset-faults/trend?${params}`, { 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(); }, [range.start, range.end, granularity, revision, refresh, scopeKey]); const drill = (status: FaultTrendStatus, period?: string) => { if (!data || loading) return; const params = new URLSearchParams({ start: data.start, end: data.end, granularity: data.granularity, status, ...scope }); if (period) params.set('period', period); setSource({ url: `/api/asset-faults/trend-analysis?${params}`, title: `故障处理趋势 · ${statusLabels[status]}`, defaultDimensions: ['handler', 'faultStatus'], periodLabel: `${data.start} 至 ${data.end}${period ? ` · ${period}` : ''} · 按提报日期` }); }; return

故障处理趋势

data ? downloadFaultReport({ kind: 'trend', data, filterLabel: scopeLabel }, chartRef.current?.querySelector('svg.recharts-surface[role=application]') || undefined) : undefined} />
{(['day', 'month'] as const).map(value => )}

按提报日期统计新增故障,展示这批故障截至当前的闭环情况。新增 = 已闭环 + 未闭环。点击柱内色块,可按运维处置人、故障工单状态查看对应日期的故障。

{error ?
故障趋势加载失败,请稍后重试。
: loading || !data ?
正在加载故障趋势…
: <>
{(['all', 'closed', 'unclosed'] as const).map(status => )}
{data.totals.reported ?
data.granularity === 'day' ? v.slice(5) : v} minTickGap={24} axisLine={false} tickLine={false} /> { const point = payload?.[0]?.payload; return point ? `${point.period} · 新增 ${point.reported} 单` : ''; }} formatter={(value, name) => [`${value} 单`, name]} /> { const period = data.periods.find(row => row.period === bar.payload?.period); if (period?.closed) drill('closed', period.period); }} fill="#2475ff" stackId="faults" maxBarSize={34} isAnimationActive={false} /> { const period = data.periods.find(row => row.period === bar.payload?.period); if (period?.unclosed) drill('unclosed', period.period); }} fill="#ff981a" stackId="faults" maxBarSize={34} isAnimationActive={false} />
:

所选时间范围暂无新增故障

} {expanded &&
{[...data.periods].reverse().map(row => {(['all', 'closed', 'unclosed'] as const).map(status => )})}
{data.granularity === 'day' ? '提报日期' : '提报月份'}新增已闭环未闭环闭环率
{row.period}{row.reported ? `${(row.closed / row.reported * 100).toFixed(1)}%` : '—'}
}

已归档视为已闭环,其余状态(含挂起)为未闭环;无提报时间不纳入趋势。按月仍仅统计所选日期范围,不扩展至整月。状态截至 {new Date(data.updatedAt).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false })}。

} {source && }
; }