import { useEffect, useRef, useState } from 'react'; import { Download, Eye, Search } from 'lucide-react'; import { useSearchParams } from 'react-router-dom'; import { adminApi, fileDownloadUrl, type ReportTask, type SingleReportMaterialDetail } from '@/api/adminApi'; import { Breadcrumb, Button, CarrierTag, DateRangeInput, Input, Modal, Pagination, Select, Table, Tag, Textarea, type DateRangeValue, type TableColumn, } from '@/components/ui'; import { formatDateTime } from '@/utils/dateTime'; import { ReportExportFormatModal, type ReportWorkbookFormat } from './ReportExportFormatModal'; function materialValue(value: unknown) { const file = value && typeof value === 'object' ? (value as Record) : {}; const fileObjectId = String(file.fileObjectId ?? ''); const fileName = String(file.fileName ?? fileObjectId ?? '-'); if ( fileObjectId && (String(file.contentType ?? '').startsWith('image/') || /\.(?:png|jpe?g|gif|webp|bmp)$/i.test(fileName)) ) return (
{fileName} {fileName}
); if (fileObjectId) return {fileName}; return String(value ?? '-'); } const statusMeta: Record = { pending: { label: '未报备', tone: 'neutral' }, waiting_material: { label: '资料待补充', tone: 'warning' }, reporting: { label: '报备中', tone: 'warning' }, exporting: { label: '报备中', tone: 'warning' }, approved: { label: '报备通过', tone: 'success' }, failed: { label: '报备失败', tone: 'danger' }, rejected: { label: '报备失败', tone: 'danger' }, abandoned: { label: '已放弃', tone: 'neutral' }, }; const actionLabels: Record = { create: '创建报备明细', batch_export: '生成报备批次', export: '生成报备文件', manual_status_change: '人工修改状态', receipt_import: '历史回执导入', }; function taskTargetLabel(task: ReportTask) { if (task.reportType !== 'drainage') return task.signature?.name ?? task.signatureId; return task.drainageInfo?.url || task.drainageItemId || '引流信息'; } function TaskDetailModal({ task, onClose }: { task: ReportTask; onClose: () => void }) { const status = statusMeta[task.status] ?? { label: task.status, tone: 'info' as const }; const source = task.exportItems?.[0]; return ( 关闭} onClose={onClose} open size="xl" title="报备明细详情">
报备对象 {taskTargetLabel(task)}
资料类型 {task.reportType === 'drainage' ? '引流信息' : '签名'}
企业 {task.signature?.tenant?.name ?? task.tenantId}
企业应用 {task.signature?.application?.name ?? '未指定应用'}
通道 {task.channel?.name ?? task.channelId}
{task.reportType !== 'drainage' ? (
运营商 {task.carrier ? : 历史通道级(未拆分)}
) : null} {task.reportType !== 'drainage' ? (
当前通过时间 {task.approvedAt ? formatDateTime(task.approvedAt) : '-'}
) : null}
当前状态 {status.label}
创建时间 {formatDateTime(task.createdAt)}
最后更新时间 {formatDateTime(task.updatedAt)}
资料版本 {source ? `V${source.batchItem.materialVersion}` : '-'}
所属批次 {source?.batchItem.batch.batchNo ?? '-'}
报备文件行 {source ? `第${source.rowNumber}行` : '-'}
当前说明 {task.reason || '-'}
{source?.exportFile.fileObjectId ? ( ) : null}

状态记录

{(task.records ?? []).length ? ( task.records!.map((record) => (
时间 {formatDateTime(record.createdAt)}
动作 {actionLabels[record.action] ?? record.action}
状态变化 {statusMeta[record.statusBefore ?? '']?.label ?? record.statusBefore ?? '-'} →{' '} {statusMeta[record.statusAfter]?.label ?? record.statusAfter}
说明 {record.reason || '-'}
)) ) : (

暂无状态记录

)}
); } export function AdminReportTasksPage() { const [searchParams] = useSearchParams(); const initialStatus = searchParams.get('scope') === 'pending' ? 'pending' : 'all'; const [tasks, setTasks] = useState([]); const [keyword, setKeyword] = useState(''); const [dateRange, setDateRange] = useState({}); const [reportType, setReportType] = useState('all'); const [status, setStatus] = useState(initialStatus); const [carrier, setCarrier] = useState('all'); const [selected, setSelected] = useState>(new Set()); const [material, setMaterial] = useState(null); const [detailTask, setDetailTask] = useState(null); const [statusTask, setStatusTask] = useState(null); const [nextStatus, setNextStatus] = useState('approved'); const [statusReason, setStatusReason] = useState(''); const [error, setError] = useState(''); const [busy, setBusy] = useState(false); const [exportTask, setExportTask] = useState(null); const [exportBusy, setExportBusy] = useState(false); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(25); const requestId = useRef(0); const [total, setTotal] = useState(0); const [appliedFilters, setAppliedFilters] = useState({ keyword: '', dateRange: {} as DateRangeValue, reportType: 'all', status: initialStatus, carrier: 'all', }); function loadData(targetPage = page, filters = appliedFilters) { const id = ++requestId.current; adminApi .listReportDetailsPage({ signatureId: searchParams.get('signatureId') || undefined, reportType: filters.reportType === 'all' ? undefined : (filters.reportType as 'signature' | 'drainage'), status: filters.status === 'all' ? undefined : filters.status, carrier: filters.carrier === 'all' ? undefined : filters.carrier, keyword: filters.keyword || undefined, createdAtFrom: filters.dateRange.start || undefined, createdAtTo: filters.dateRange.end || undefined, page: targetPage, pageSize, }) .then((result) => { if (id !== requestId.current) return; setTasks(result.items); setTotal(result.total); setSelected(new Set()); setError(''); }) .catch((failure: Error) => { if (id === requestId.current) setError(failure.message || '报备明细加载失败'); }); } useEffect(() => { loadData(page); return () => { requestId.current += 1; }; }, [page, pageSize]); async function saveTaskStatus() { if (!statusTask) return; const chosen = selected.size ? tasks.filter((task) => selected.has(task.id)) : [statusTask]; setBusy(true); try { await adminApi.changeReportTaskStatuses({ items: chosen.map((task) => ({ signatureId: task.signatureId, channelId: task.channelId, carrier: task.carrier ?? undefined, reportType: task.reportType, drainageItemId: task.drainageItemId ?? undefined, status: nextStatus, })), reason: statusReason.trim() || undefined, sourceEntry: 'report_task', }); setStatusTask(null); setStatusReason(''); setSelected(new Set()); loadData(); } catch (failure) { setError(failure instanceof Error ? failure.message : '报备状态保存失败'); } finally { setBusy(false); } } async function openMaterial(task: ReportTask) { try { setMaterial( await adminApi.getSingleReportMaterialDetail({ reportType: task.reportType, signatureId: task.signatureId, channelId: task.channelId, carrier: task.carrier ?? undefined, drainageItemId: task.drainageItemId ?? undefined, batchItemId: task.exportItems?.[0]?.batchItem.id, }), ); } catch (failure) { setError(failure instanceof Error ? failure.message : '报备资料加载失败'); } } async function exportMaterial(task: ReportTask, outputFormat: ReportWorkbookFormat) { try { setExportBusy(true); const blob = await adminApi.exportSingleReportMaterial({ reportType: task.reportType, signatureId: task.signatureId, channelId: task.channelId, carrier: task.carrier ?? undefined, drainageItemId: task.drainageItemId ?? undefined, batchItemId: task.exportItems?.[0]?.batchItem.id, outputFormat, }); const url = URL.createObjectURL(blob); const anchor = document.createElement('a'); anchor.href = url; anchor.download = `${task.signature?.name ?? '签名'}-${task.channel?.name ?? '通道'}.xlsx`; anchor.click(); URL.revokeObjectURL(url); setExportTask(null); } catch (failure) { setError(failure instanceof Error ? failure.message : '单条资料导出失败'); } finally { setExportBusy(false); } } const columns: Array> = [ { key: 'select', title: '', width: '44px', render: (record) => ( setSelected((current) => { const next = new Set(current); if (next.has(record.id)) next.delete(record.id); else next.add(record.id); return next; }) } type="checkbox" /> ), }, { key: 'target', title: '报备对象', render: (record) => (
{taskTargetLabel(record)}
{record.reportType === 'drainage' ? '引流信息' : '签名'} ·{' '} {record.signature?.tenant?.name ?? record.tenantId}
), }, { key: 'application', title: '企业应用', render: (record) => record.signature?.application?.name ?? '未指定应用' }, { key: 'channel', title: '通道/运营商', render: (record) => (
{record.channel?.name ?? record.channelId}
{record.carrier ? : '历史通道级(未拆分)'}
), }, { key: 'batch', title: '批次/版本', render: (record) => { const source = record.exportItems?.[0]; return source ? (
{source.batchItem.batch.batchNo}
V{source.batchItem.materialVersion} · 第{source.rowNumber}行
) : ( '-' ); }, }, { key: 'status', title: '状态', render: (record) => ( {(statusMeta[record.status] ?? { label: record.status }).label} ), }, { key: 'time', title: '更新时间', render: (record) => formatDateTime(record.updatedAt ?? record.createdAt) }, { key: 'actions', title: '操作', align: 'right', render: (record) => (
{record.reportType !== 'drainage' ? ( ) : null}
), }, ]; const allCurrentPageSelected = tasks.length > 0 && tasks.every((task) => selected.has(task.id)); return (

通道报备明细

按企业应用 × 签名/引流对象 × 通道 × 运营商展示,未生成任务的“未报备”明细也会显示。

{error ?

{error}

: null}
setKeyword(event.target.value)} placeholder="搜索报备明细" value={keyword} /> setCarrier(event.target.value)} options={[ { label: '全部运营商', value: 'all' }, { label: '移动', value: 'mobile' }, { label: '联通', value: 'unicom' }, { label: '电信', value: 'telecom' }, ]} value={carrier} /> setNextStatus(event.target.value)} options={[ { label: '未报备', value: 'pending' }, { label: '资料待补充', value: 'waiting_material' }, { label: '报备中', value: 'reporting' }, { label: '报备通过', value: 'approved' }, { label: '报备失败', value: 'failed' }, { label: '放弃报备', value: 'abandoned' }, ]} value={nextStatus} />