import { useCallback, useEffect, useRef, useState } from 'react'; import { downloadAdminErrorReport, formatAdminApiError, getAdminErrorReport, isAdminApiError, listAdminErrorReports, updateAdminErrorReport, } from '../api/adminApiClient'; import type { AdminErrorReportDetail, AdminErrorReportEntry, } from '../api/adminApiTypes'; type Props = { token: string; onUnauthorized: (message?: string) => void }; const ADMIN_ERROR_REPORT_STATUSES = ['new', 'in-progress', 'resolved'] as const; const ERROR_REPORT_PAGE_SIZE = 50; export function AdminErrorReportsPage({ token, onUnauthorized }: Props) { const [reports, setReports] = useState([]); const [selected, setSelected] = useState(null); const [status, setStatus] = useState(''); const [downloading, setDownloading] = useState(false); const [busy, setBusy] = useState(false); const [filterStatus, setFilterStatus] = useState(''); const [pageOffset, setPageOffset] = useState(0); const [pageInfo, setPageInfo] = useState({ total: 0, hasMore: false }); const openReportRequestId = useRef(0); const loadRequestId = useRef(0); const load = useCallback(async () => { const requestId = ++loadRequestId.current; setStatus(''); try { const response = await listAdminErrorReports(token, { status: filterStatus || undefined, limit: ERROR_REPORT_PAGE_SIZE, offset: pageOffset, }); if (requestId === loadRequestId.current) { const lastValidOffset = response.total ? Math.floor((response.total - 1) / ERROR_REPORT_PAGE_SIZE) * ERROR_REPORT_PAGE_SIZE : 0; if (pageOffset > lastValidOffset) { setPageOffset(lastValidOffset); return; } setReports(response.reports); setPageInfo({ total: response.total, hasMore: response.hasMore }); } } catch (error) { if (requestId !== loadRequestId.current) return; if (isAdminApiError(error) && error.status === 401) return onUnauthorized(); setStatus(formatAdminApiError(error)); } }, [filterStatus, onUnauthorized, pageOffset, token]); const loadRef = useRef(load); useEffect(() => { loadRef.current = load; }, [load]); useEffect(() => { void load(); }, [load]); async function openReport(batchId: string) { const requestId = ++openReportRequestId.current; setStatus(''); try { const detail = await getAdminErrorReport(token, batchId); if (requestId === openReportRequestId.current) setSelected(detail); } catch (error) { if (requestId !== openReportRequestId.current) return; if (isAdminApiError(error) && error.status === 401) return onUnauthorized(); setStatus(formatAdminApiError(error)); } } async function saveStatus(nextStatus: string) { if (!selected || busy) return; setBusy(true); setStatus(''); try { const updated = await updateAdminErrorReport(token, selected.batchId, { status: nextStatus, note: selected.note, }); setSelected((current) => current ? { ...current, ...updated } : current, ); await loadRef.current(); } catch (error) { if (isAdminApiError(error) && error.status === 401) onUnauthorized(); else setStatus(formatAdminApiError(error)); } finally { setBusy(false); } } async function download(batchId: string) { if (downloading) return; setDownloading(true); setStatus(''); try { const blob = await downloadAdminErrorReport(token, batchId); const url = URL.createObjectURL(blob); const anchor = document.createElement('a'); anchor.href = url; anchor.download = `${batchId}.zip`; document.body.appendChild(anchor); anchor.click(); anchor.remove(); setTimeout(() => URL.revokeObjectURL(url), 10_000); } catch (error) { if (isAdminApiError(error) && error.status === 401) onUnauthorized(); else setStatus(formatAdminApiError(error)); } finally { setDownloading(false); } } return (

错误报告

查看用户提交的脱敏诊断包。

{status ? (

{status}

) : null}
{reports.map((report) => ( void openReport(report.batchId)} role="button" tabIndex={0} onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); void openReport(report.batchId); } }} > ))}
报告 事件 来源 状态 用户 时间
{report.batchId} {report.eventCount} {report.source ?? '-'} {report.status} {report.userId} {new Date(report.createdAt).toLocaleString()}
{pageInfo.total === 0 ? '暂无报告' : `第 ${pageOffset + 1}-${Math.min(pageOffset + reports.length, pageInfo.total)} 条,共 ${pageInfo.total} 条`}
{selected ? (

{selected.batchId}

用户:{selected.userId} · 事件:{selected.eventCount} · 日志: {selected.logCount}

{JSON.stringify(selected.events.slice(0, 20), null, 2)}
{selected.userDescription ? (

用户描述:{selected.userDescription}

) : null}