feat: add report material workflows and gateway safeguards

This commit is contained in:
hectorzhao
2026-07-15 18:23:48 +08:00
parent cf9f4ce4cd
commit 7091a8bed4
41 changed files with 3606 additions and 71 deletions
@@ -0,0 +1,54 @@
import { useEffect, useMemo, useState } from 'react';
import { Download, FileSpreadsheet, Layers3, RefreshCw } from 'lucide-react';
import { adminApi, fileDownloadUrl, type ReportMaterialPendingItem } from '@/api/adminApi';
import { Breadcrumb, Button, Input, Select, Tag } from '@/components/ui';
import { formatDateTime } from '@/utils/dateTime';
import { ReportMaterialImportModal } from './ReportMaterialImportModal';
type Batch = Record<string, unknown> & { id: string; batchNo?: string; status?: string; createdAt?: string; selectedCount?: number; channelCount?: number; exportFiles?: Array<Record<string, unknown>> };
const statusLabel: Record<string, string> = { completed: '生成完成', partial_failed: '部分资料待补充', failed: '生成失败', processing: '生成中' };
export function AdminReportMaterialsPage() {
const [items, setItems] = useState<ReportMaterialPendingItem[]>([]);
const [batches, setBatches] = useState<Batch[]>([]);
const [selected, setSelected] = useState<Set<string>>(new Set());
const [reportType, setReportType] = useState('all');
const [keyword, setKeyword] = useState('');
const [importOpen, setImportOpen] = useState(false);
const [busy, setBusy] = useState(false);
const [error, setError] = useState('');
const [message, setMessage] = useState('');
function loadData() {
Promise.all([adminApi.listPendingReportMaterials(reportType === 'all' ? {} : { reportType: reportType as 'signature' | 'drainage' }), adminApi.listReportMaterialBatches()])
.then(([pendingItems, batchItems]) => { setItems(pendingItems); setBatches(batchItems as Batch[]); setSelected((current) => new Set([...current].filter((id) => pendingItems.some((item) => item.id === id)))); setError(''); })
.catch((failure: Error) => setError(failure.message || '待报备资料加载失败'));
}
useEffect(loadData, [reportType]);
const visibleItems = useMemo(() => items.filter((item) => !keyword.trim() || [item.name, item.detail, item.signatureName, item.tenant?.name, item.application?.name].some((value) => String(value ?? '').includes(keyword.trim()))), [items, keyword]);
const allSelected = visibleItems.length > 0 && visibleItems.every((item) => selected.has(item.id));
function toggle(id: string) { setSelected((current) => { const next = new Set(current); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }
async function createBatch() {
const chosen = items.filter((item) => selected.has(item.id));
if (!chosen.length) { setError('请先选择要统一报备的资料'); return; }
setBusy(true); setError(''); setMessage('');
try {
const batch = await adminApi.createReportMaterialBatch({ items: chosen.map((item) => ({ reportType: item.reportType, signatureId: item.signatureId, drainageItemId: item.drainageItemId ?? undefined })) });
setMessage(`批次 ${String(batch.batchNo ?? '')} 已按应用路由生成各通道报备文件`); setSelected(new Set()); loadData();
} catch (failure) { setError(failure instanceof Error ? failure.message : '报备批次生成失败'); }
finally { setBusy(false); }
}
return <section className="page-stack report-material-page">
<div className="surface page-heading"><div><Breadcrumb items={['报备任务', '待报备资料']} /><h1></h1><p> XLSX </p></div><div className="page-heading-actions"><Button icon={<FileSpreadsheet size={16} />} onClick={() => setImportOpen(true)} variant="ghost"> WPS </Button><Button disabled={busy || selected.size === 0} icon={<Layers3 size={16} />} onClick={() => void createBatch()}>{busy ? '生成中...' : `统一生成通道报备(${selected.size}`}</Button></div></div>
{error ? <p className="form-error">{error}</p> : null}{message ? <p className="form-success">{message}</p> : null}
<div className="surface report-material-filter"><Select label="资料类型" onChange={(event) => setReportType(event.target.value)} options={[{ label: '全部资料', value: 'all' }, { label: '签名资料', value: 'signature' }, { label: '引流信息', value: 'drainage' }]} value={reportType} /><Input label="企业/应用/签名/站点" onChange={(event) => setKeyword(event.target.value)} placeholder="搜索待报备资料" value={keyword} /><Button icon={<RefreshCw size={15} />} onClick={loadData} variant="ghost"></Button></div>
<div className="surface report-material-pool"><div className="report-material-table-head"><input checked={allSelected} onChange={() => setSelected((current) => { const next = new Set(current); for (const item of visibleItems) allSelected ? next.delete(item.id) : next.add(item.id); return next; })} type="checkbox" /><span></span><span> / </span><span></span><span></span></div>{visibleItems.map((item) => <label className="report-material-row" key={item.id}><input checked={selected.has(item.id)} onChange={() => toggle(item.id)} type="checkbox" /><span><strong>{item.name}</strong><small>{item.reportType === 'signature' ? '签名资料' : `引流信息 · 所属签名 ${item.signatureName ?? '-'}`}</small><em>{item.detail || '-'}</em></span><span><strong>{item.tenant?.name ?? '-'}</strong><small>{item.application?.name ?? '未指定应用'}</small></span><Tag tone="info">V{item.materialVersion}</Tag><span>{formatDateTime(item.changedAt)}</span></label>)}{visibleItems.length === 0 ? <div className="channel-report-empty"></div> : null}</div>
<div className="surface report-material-batches"><div className="channel-field-section-head"><div><h2></h2><p> XLSX</p></div><Tag tone="neutral">{batches.length} </Tag></div>{batches.map((batch) => <article key={batch.id}><div><strong>{batch.batchNo ?? batch.id}</strong><span>{statusLabel[String(batch.status)] ?? batch.status}</span><small>{batch.createdAt ? formatDateTime(batch.createdAt) : '-'} · {batch.selectedCount ?? 0} · {batch.channelCount ?? 0} </small></div><div>{(batch.exportFiles ?? []).map((file) => <a href={fileDownloadUrl(String(file.fileObjectId))} key={String(file.id)}><Download size={15} />{String(file.fileName ?? '下载报备文件')}{String(file.rowCount ?? 0)} </a>)}</div></article>)}{batches.length === 0 ? <div className="channel-report-empty"></div> : null}</div>
{importOpen ? <ReportMaterialImportModal onClose={() => setImportOpen(false)} onCompleted={() => { setMessage('资料导入完成,已进入待报备池'); loadData(); }} /> : null}
</section>;
}