import { useEffect, useMemo, useState } from 'react'; import { AlertTriangle, CheckCircle2, Download, Layers3, Search, ShieldCheck } from 'lucide-react'; import { adminApi, fileDownloadUrl, type ReportMaterialBatch, type ReportMaterialBatchPreflight, type ReportMaterialBatchResult, type ReportMaterialPendingItem, } from '@/api/adminApi'; import { Breadcrumb, Button, DateRangeInput, Input, Modal, Pagination, Select, Table, Tabs, Tag, type DateRangeValue, type TableColumn, } from '@/components/ui'; import { formatDateTime } from '@/utils/dateTime'; import { createUuid } from '@/utils/randomId'; const batchStatusLabels: Record = { completed: '生成完成', partial_failed: '部分生成', failed: '生成失败', generating: '生成中', processing: '生成中', }; export function AdminReportMaterialsPage() { const [activeTab, setActiveTab] = useState<'pending' | 'batches'>('pending'); const [pendingData, setPendingData] = useState<{ items: ReportMaterialPendingItem[]; total: number }>({ items: [], total: 0 }); const [batchData, setBatchData] = useState<{ items: ReportMaterialBatch[]; total: number }>({ items: [], total: 0 }); const [selected, setSelected] = useState>(new Set()); const [reportType, setReportType] = useState('all'); const [keyword, setKeyword] = useState(''); const [dateRange, setDateRange] = useState({}); const [pendingPage, setPendingPage] = useState(1); const [batchPage, setBatchPage] = useState(1); const pageSize = 20; const [busy, setBusy] = useState(false); const [preflightBusy, setPreflightBusy] = useState(false); const [preflight, setPreflight] = useState(null); const [poolEligibility, setPoolEligibility] = useState>(new Map()); const [operationKey, setOperationKey] = useState(''); const [batchResult, setBatchResult] = useState(null); const [confirmOpen, setConfirmOpen] = useState(false); const [error, setError] = useState(''); const [message, setMessage] = useState(''); async function loadPending( page = pendingPage, filters: { keyword?: string; dateRange?: DateRangeValue; reportType?: string; } = {}, ) { const nextKeyword = filters.keyword ?? keyword; const nextDateRange = filters.dateRange ?? dateRange; const nextReportType = filters.reportType ?? reportType; try { const result = await adminApi.listPendingReportMaterials({ reportType: nextReportType === 'all' ? undefined : nextReportType as 'signature' | 'drainage', keyword: nextKeyword.trim() || undefined, startAt: nextDateRange.start, endAt: nextDateRange.end, page, pageSize, }); setPendingData({ items: result.items, total: result.total }); const eligibility = result.items.length ? await adminApi.preflightReportMaterialBatch({ items: result.items.map(toBatchItem) }) : null; const eligibilityMap = new Map((eligibility?.items ?? []).map((item) => [item.id, item])); setPoolEligibility(eligibilityMap); setSelected((current) => new Set([...current].filter((id) => eligibilityMap.get(id)?.eligible))); setError(''); } catch (failure) { setError(failure instanceof Error ? failure.message : '待生成资料加载失败'); } } async function loadBatches( page = batchPage, filters: { keyword?: string; dateRange?: DateRangeValue; } = {}, ) { const nextKeyword = filters.keyword ?? keyword; const nextDateRange = filters.dateRange ?? dateRange; try { const result = await adminApi.listReportMaterialBatches({ keyword: nextKeyword.trim() || undefined, startAt: nextDateRange.start, endAt: nextDateRange.end, page, pageSize, }); setBatchData({ items: result.items, total: result.total }); setError(''); } catch (failure) { setError(failure instanceof Error ? failure.message : '已生成批次加载失败'); } } function loadActive() { if (activeTab === 'pending') void loadPending(pendingPage); else void loadBatches(batchPage); } useEffect(() => { loadActive(); }, [activeTab, pendingPage, batchPage, reportType]); const eligibleItems = pendingData.items.filter((item) => poolEligibility.get(item.id)?.eligible); const allSelected = eligibleItems.length > 0 && eligibleItems.every((item) => selected.has(item.id)); function toggle(id: string) { if (!poolEligibility.get(id)?.eligible) return; setSelected((current) => { const next = new Set(current); if (next.has(id)) next.delete(id); else next.add(id); return next; }); } async function beginCreateBatch() { const chosen = pendingData.items.filter((item) => selected.has(item.id)); if (!chosen.length) { setError('请先选择具备报备资格的资料'); return; } setConfirmOpen(true); setPreflightBusy(true); setPreflight(null); setBatchResult(null); setError(''); setMessage(''); setOperationKey(`report-batch:${createUuid()}`); try { setPreflight(await adminApi.preflightReportMaterialBatch({ items: chosen.map(toBatchItem) })); } catch (failure) { setError(failure instanceof Error ? failure.message : '报备资格预检失败'); } finally { setPreflightBusy(false); } } async function createBatch() { const chosen = pendingData.items.filter((item) => selected.has(item.id)); if (!chosen.length) return; setBusy(true); setError(''); try { const batch = await adminApi.createReportMaterialBatch({ idempotencyKey: operationKey, items: chosen.map((item) => ({ ...toBatchItem(item), materialVersion: item.materialVersion })), }); setBatchResult(batch); setMessage(`批次 ${batch.batchNo} 已生成:成功 ${batch.result.successCount},跳过 ${batch.result.skippedCount},失败 ${batch.result.failedCount}`); setSelected(new Set()); await Promise.all([loadPending(pendingPage), loadBatches(1)]); } catch (failure) { setError(failure instanceof Error ? failure.message : '报备批次生成失败'); } finally { setBusy(false); } } const pendingColumns = useMemo>>(() => [ { key: 'select', title: '', width: '48px', render: (item) => { const eligible = poolEligibility.get(item.id)?.eligible; return toggle(item.id)} type="checkbox" />; }, }, { key: 'name', title: '资料', render: (item) =>
{item.name}
{item.reportType === 'signature' ? '签名资料' : `引流信息 · ${item.signatureName ?? '-'}`} · {item.detail || '-'}
}, { key: 'tenant', title: '企业/应用', render: (item) =>
{item.tenant?.name ?? '-'}
{item.application?.name ?? '未指定应用'}
}, { key: 'eligibility', title: '版本/资格', render: (item) => { const eligibility = poolEligibility.get(item.id); const eligible = eligibility?.eligible; return
V{item.materialVersion} · {eligible ? `${eligibility.targets.filter((target) => target.eligible).length}个通道可生成` : '待补充'}{!eligible ?
{eligibility?.blockedReasons[0] ?? '资格检查中'}
: null}
; } }, { key: 'changedAt', title: '资料变更时间', render: (item) => formatDateTime(item.changedAt) }, ], [poolEligibility, selected]); const batchColumns = useMemo>>(() => [ { key: 'batchNo', title: '报备批次号', render: (batch) => {batch.batchNo} }, { key: 'time', title: '生成时间', render: (batch) => formatDateTime(batch.createdAt) }, { key: 'reportTotal', title: '报备总数', render: (batch) => batch.reportTotal.toLocaleString('zh-CN') }, { key: 'successCount', title: '成功数', render: (batch) => batch.successCount.toLocaleString('zh-CN') }, { key: 'successRate', title: '成功率', render: (batch) => `${(batch.successRate * 100).toFixed(2)}%` }, { key: 'channels', title: '通道/文件', render: (batch) => `${batch.channelCount}个通道 · ${batch.fileCount}份文件` }, { key: 'status', title: '生成状态', render: (batch) => {batchStatusLabels[batch.status] ?? batch.status} }, { key: 'files', title: '报备文件', align: 'right', render: (batch) =>
{batch.exportFiles.map((file) => file.fileObjectId ? {file.fileName}({file.rowCount}行) : null)}
}, ], []); const filter =
{activeTab === 'pending' ? setKeyword(event.target.value)} placeholder={activeTab === 'pending' ? '搜索待生成资料' : '搜索报备批次号'} value={keyword} />
; return

待生成报备批次

审核通过的签名和引流资料先进入待生成池,运营选择资料后按应用路由为各通道生成批量报备文件。

{activeTab === 'pending' ? : null}
{error ?

{error}

: null} {message ?

{message}

: null} { setActiveTab(value as 'pending' | 'batches'); setKeyword(''); setDateRange({}); }} value={activeTab} items={[ { label: `待生成资料(${pendingData.total})`, value: 'pending', content:
{filter}
= pendingData.total} onNext={() => setPendingPage((page) => page + 1)} onPageChange={setPendingPage} onPrevious={() => setPendingPage((page) => Math.max(1, page - 1))} page={pendingPage} previousDisabled={pendingPage <= 1} total={pendingData.total} totalPages={Math.max(1, Math.ceil(pendingData.total / pageSize))} />, }, { label: `已生成批次(${batchData.total})`, value: 'batches', content:
{filter}
= batchData.total} onNext={() => setBatchPage((page) => page + 1)} onPageChange={setBatchPage} onPrevious={() => setBatchPage((page) => Math.max(1, page - 1))} page={batchPage} previousDisabled={batchPage <= 1} total={batchData.total} totalPages={Math.max(1, Math.ceil(batchData.total / pageSize))} />, }, ]} /> setConfirmOpen(false)}>关闭 : <>} onClose={() => { if (!busy) setConfirmOpen(false); }} open={confirmOpen} size="xl" title="报备生成资格预检">
{preflightBusy ?

正在核对资料版本、应用路由、通道字段与历史批次…

: null} {preflight ? <>
可生成 {preflight.eligibleTargetCount} 个资料通道组合跳过 {preflight.skippedTargetCount} 个组合
{preflight.items.map((item) =>
{item.name}{item.tenantName} · {item.applicationName} · V{item.materialVersion}
{item.targets.length ?
    {item.targets.map((target) =>
  • {target.name} · {target.carrier}{target.eligible ? '资格通过' : target.blockedReasons.join(';')}
  • )}
:

{item.blockedReasons.join(';')}

}
)} : null} {batchResult ?
报备批次 {batchResult.batchNo} 已处理成功 {batchResult.result.successCount} · 跳过 {batchResult.result.skippedCount} · 失败 {batchResult.result.failedCount}操作单号:{batchResult.operationId}{batchResult.replayed ? '(幂等重放)' : ''}
: null}
; } function toBatchItem(item: ReportMaterialPendingItem) { return { reportType: item.reportType, signatureId: item.signatureId, drainageItemId: item.drainageItemId ?? undefined, materialVersion: item.materialVersion }; }