feat: build reporting workbench workflow
This commit is contained in:
@@ -0,0 +1,339 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Download, Eye, Search } from 'lucide-react';
|
||||
import { adminApi, fileDownloadUrl, type ReportMaterialBatch, type ReportTask } 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';
|
||||
|
||||
const statusLabels: Record<string, string> = {
|
||||
completed: '生成完成',
|
||||
partial_failed: '部分生成',
|
||||
failed: '生成失败',
|
||||
generating: '生成中',
|
||||
pending: '未报备',
|
||||
waiting_material: '资料待补充',
|
||||
reporting: '报备中',
|
||||
approved: '报备通过',
|
||||
rejected: '报备失败',
|
||||
abandoned: '已放弃',
|
||||
};
|
||||
|
||||
export function AdminReportBatchesPage() {
|
||||
const [items, setItems] = useState<ReportMaterialBatch[]>([]);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [dateRange, setDateRange] = useState<DateRangeValue>({});
|
||||
const [page, setPage] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [detail, setDetail] = useState<ReportMaterialBatch>();
|
||||
const [tasks, setTasks] = useState<ReportTask[]>([]);
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||
const [nextStatus, setNextStatus] = useState('reporting');
|
||||
const [reason, setReason] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const pageSize = 20;
|
||||
|
||||
function load(target = page) {
|
||||
adminApi
|
||||
.listReportMaterialBatches({
|
||||
keyword: keyword.trim() || undefined,
|
||||
startAt: dateRange.start,
|
||||
endAt: dateRange.end,
|
||||
page: target,
|
||||
pageSize,
|
||||
})
|
||||
.then((result) => {
|
||||
setItems(result.items);
|
||||
setTotal(result.total);
|
||||
setError('');
|
||||
})
|
||||
.catch((failure: Error) => setError(failure.message || '报备批次加载失败'));
|
||||
}
|
||||
useEffect(() => {
|
||||
load(page);
|
||||
}, [page]);
|
||||
|
||||
async function openBatch(batch: ReportMaterialBatch) {
|
||||
try {
|
||||
const [batchDetail, taskPage] = await Promise.all([
|
||||
adminApi.getReportMaterialBatch(batch.id),
|
||||
adminApi.listReportMaterialBatchTasks(batch.id, { page: 1, pageSize: 100 }),
|
||||
]);
|
||||
setDetail(batchDetail);
|
||||
setTasks(taskPage.items);
|
||||
setSelected(new Set());
|
||||
setError('');
|
||||
} catch (failure) {
|
||||
setError(failure instanceof Error ? failure.message : '批次明细加载失败');
|
||||
}
|
||||
}
|
||||
async function saveStatuses() {
|
||||
const chosen = tasks.filter((task) => selected.has(task.id));
|
||||
if (!chosen.length) return;
|
||||
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: reason.trim() || undefined,
|
||||
sourceEntry: 'report_task',
|
||||
});
|
||||
if (detail) await openBatch(detail);
|
||||
} catch (failure) {
|
||||
setError(failure instanceof Error ? failure.message : '批量状态修改失败');
|
||||
}
|
||||
}
|
||||
async function exportOne(task: ReportTask) {
|
||||
try {
|
||||
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,
|
||||
});
|
||||
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);
|
||||
} catch (failure) {
|
||||
setError(failure instanceof Error ? failure.message : '单条资料导出失败');
|
||||
}
|
||||
}
|
||||
|
||||
const columns: Array<TableColumn<ReportMaterialBatch>> = [
|
||||
{ key: 'batchNo', title: '报备批次号', render: (item) => <strong>{item.batchNo}</strong> },
|
||||
{ key: 'time', title: '生成时间', render: (item) => formatDateTime(item.createdAt) },
|
||||
{ key: 'count', title: '明细进度', render: (item) => `${item.successCount}/${item.reportTotal}` },
|
||||
{ key: 'channels', title: '通道/文件', render: (item) => `${item.channelCount}个通道 · ${item.fileCount}份文件` },
|
||||
{
|
||||
key: 'status',
|
||||
title: '生成状态',
|
||||
render: (item) => (
|
||||
<Tag tone={item.status === 'completed' ? 'success' : item.status === 'failed' ? 'danger' : 'warning'}>
|
||||
{statusLabels[item.status] ?? item.status}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'files',
|
||||
title: '文件',
|
||||
render: (item) => (
|
||||
<div className="table-actions">
|
||||
{item.exportFiles.map((file) =>
|
||||
file.fileObjectId ? (
|
||||
<a href={fileDownloadUrl(file.fileObjectId)} key={file.id}>
|
||||
<Download size={14} />
|
||||
{file.fileName}
|
||||
</a>
|
||||
) : null,
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
title: '操作',
|
||||
align: 'right',
|
||||
render: (item) => (
|
||||
<Button icon={<Eye size={14} />} onClick={() => void openBatch(item)} size="sm" variant="ghost">
|
||||
打开明细
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
const taskColumns: Array<TableColumn<ReportTask>> = [
|
||||
{
|
||||
key: 'select',
|
||||
title: '',
|
||||
width: '44px',
|
||||
render: (task) => (
|
||||
<input
|
||||
aria-label={`选择${task.signature?.name ?? task.id}`}
|
||||
checked={selected.has(task.id)}
|
||||
onChange={() =>
|
||||
setSelected((current) => {
|
||||
const next = new Set(current);
|
||||
if (next.has(task.id)) next.delete(task.id);
|
||||
else next.add(task.id);
|
||||
return next;
|
||||
})
|
||||
}
|
||||
type="checkbox"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'target',
|
||||
title: '企业/应用/签名',
|
||||
render: (task) => (
|
||||
<div>
|
||||
<strong>{task.signature?.name ?? '-'}</strong>
|
||||
<div className="muted">
|
||||
{task.signature?.tenant?.name ?? '-'} · {task.signature?.application?.name ?? '-'}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'channel',
|
||||
title: '通道/运营商',
|
||||
render: (task) => (
|
||||
<div>
|
||||
{task.channel?.name ?? '-'}
|
||||
{task.carrier ? (
|
||||
<div>
|
||||
<CarrierTag carrier={task.carrier} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'version',
|
||||
title: '资料版本',
|
||||
render: (task) => `V${task.exportItems?.[0]?.batchItem.materialVersion ?? '-'}`,
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
title: '报备状态',
|
||||
render: (task) => (
|
||||
<Tag
|
||||
tone={
|
||||
task.status === 'approved'
|
||||
? 'success'
|
||||
: task.status === 'failed' || task.status === 'rejected'
|
||||
? 'danger'
|
||||
: 'warning'
|
||||
}
|
||||
>
|
||||
{statusLabels[task.status] ?? task.status}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
title: '操作',
|
||||
align: 'right',
|
||||
render: (task) =>
|
||||
task.reportType !== 'drainage' ? (
|
||||
<Button icon={<Download size={14} />} onClick={() => void exportOne(task)} size="sm" variant="ghost">
|
||||
导出本条
|
||||
</Button>
|
||||
) : (
|
||||
'-'
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="page-stack report-batch-page">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<Breadcrumb items={['报备工作台', '报备批次']} />
|
||||
<h1>报备批次</h1>
|
||||
<p>查看已生成批次、下载通道文件,并在批次内批量处理通道报备明细。</p>
|
||||
</div>
|
||||
</div>
|
||||
{error ? <p className="form-error">{error}</p> : null}
|
||||
<div className="surface admin-task-filter">
|
||||
<Input label="报备批次号" onChange={(event) => setKeyword(event.target.value)} value={keyword} />
|
||||
<DateRangeInput label="生成时间" onChange={setDateRange} value={dateRange} />
|
||||
<div className="admin-task-filter__actions">
|
||||
<Button
|
||||
icon={<Search size={16} />}
|
||||
onClick={() => {
|
||||
if (page !== 1) setPage(1);
|
||||
else load(1);
|
||||
}}
|
||||
>
|
||||
查询
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setKeyword('');
|
||||
setDateRange({});
|
||||
}}
|
||||
variant="ghost"
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="surface">
|
||||
<Table columns={columns} data={items} emptyText="尚未生成报备批次" pagination={false} rowKey="id" />
|
||||
</div>
|
||||
<Pagination
|
||||
nextDisabled={page * pageSize >= total}
|
||||
onNext={() => setPage((value) => value + 1)}
|
||||
onPageChange={setPage}
|
||||
onPrevious={() => setPage((value) => Math.max(1, value - 1))}
|
||||
page={page}
|
||||
previousDisabled={page <= 1}
|
||||
total={total}
|
||||
totalPages={Math.max(1, Math.ceil(total / pageSize))}
|
||||
/>
|
||||
{detail ? (
|
||||
<Modal
|
||||
footer={<Button onClick={() => setDetail(undefined)}>关闭</Button>}
|
||||
onClose={() => setDetail(undefined)}
|
||||
open
|
||||
size="xl"
|
||||
title={`批次明细 · ${detail.batchNo}`}
|
||||
>
|
||||
<div className="page-stack">
|
||||
<div className="report-batch-toolbar">
|
||||
<strong>
|
||||
共 {tasks.length} 条通道明细,已选 {selected.size} 条
|
||||
</strong>
|
||||
<Select
|
||||
aria-label="批量修改状态"
|
||||
onChange={(event) => 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}
|
||||
/>
|
||||
<Textarea
|
||||
aria-label="修改原因"
|
||||
onChange={(event) => setReason(event.target.value)}
|
||||
placeholder="修改原因"
|
||||
rows={2}
|
||||
value={reason}
|
||||
/>
|
||||
<Button disabled={!selected.size} onClick={() => void saveStatuses()}>
|
||||
批量修改
|
||||
</Button>
|
||||
</div>
|
||||
<Table columns={taskColumns} data={tasks} emptyText="该批次暂无明细" pagination={false} rowKey="id" />
|
||||
</div>
|
||||
</Modal>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user