fix: 修复报备导入映射与审核跳转

This commit is contained in:
hectorzhao
2026-09-05 00:20:33 +08:00
parent 65082959c0
commit 1a7a7245a6
12 changed files with 879 additions and 159 deletions
+300 -83
View File
@@ -1,7 +1,19 @@
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { Check, Eye, Search, X } from 'lucide-react';
import { adminApi, type ReportImportReviewBatch, type ReportImportReviewItem } from '@/api/adminApi';
import { Button, DateRangeInput, Input, Modal, Pagination, Select, Table, Tag, Textarea, type DateRangeValue, type TableColumn } from '@/components/ui';
import {
Button,
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> = {
@@ -32,8 +44,17 @@ function tone(status: string): 'neutral' | 'info' | 'success' | 'warning' | 'dan
return status === 'pending_review' ? 'info' : 'neutral';
}
export function ReportImportAuditPanel({ reportType }: { reportType: 'signature' | 'drainage' }) {
const [data, setData] = useState<{ items: ReportImportReviewBatch[]; total: number; page: number; pageSize: number }>({ items: [], total: 0, page: 1, pageSize: 20 });
export function ReportImportAuditPanel({
reportType,
initialBatchId,
}: {
reportType: 'signature' | 'drainage';
initialBatchId?: string;
}) {
const initialBatchHandled = useRef(false);
const [data, setData] = useState<{ items: ReportImportReviewBatch[]; total: number; page: number; pageSize: number }>(
{ items: [], total: 0, page: 1, pageSize: 20 },
);
const [page, setPage] = useState(1);
const pageSize = 20;
const [keyword, setKeyword] = useState('');
@@ -47,19 +68,25 @@ export function ReportImportAuditPanel({ reportType }: { reportType: 'signature'
const [error, setError] = useState('');
function load(targetPage = page) {
adminApi.listReportImportReviewBatches({
reportType,
status: status === 'all' ? undefined : status,
keyword: keyword.trim() || undefined,
startAt: submittedDateRange.start,
endAt: submittedDateRange.end,
page: targetPage,
pageSize,
}).then((result) => {
setData(result);
setError('');
if (detail) setDetail(result.items.find((item) => item.id === detail.id));
}).catch((failure: Error) => setError(failure.message || '导入审核批次加载失败'));
adminApi
.listReportImportReviewBatches({
reportType,
status: status === 'all' ? undefined : status,
keyword: keyword.trim() || undefined,
startAt: submittedDateRange.start,
endAt: submittedDateRange.end,
page: targetPage,
pageSize,
})
.then((result) => {
setData(result);
setError('');
if (!initialBatchHandled.current && initialBatchId) {
setDetail(result.items.find((item) => item.id === initialBatchId));
initialBatchHandled.current = true;
} else if (detail) setDetail(result.items.find((item) => item.id === detail.id));
})
.catch((failure: Error) => setError(failure.message || '导入审核批次加载失败'));
}
useEffect(() => {
@@ -98,78 +125,268 @@ export function ReportImportAuditPanel({ reportType }: { reportType: 'signature'
}
}
const columns = useMemo<Array<TableColumn<ReportImportReviewBatch>>>(() => [
{ key: 'file', title: '导入批次/文件', render: (record) => <div><strong>{record.id}</strong><div className="muted">{record.fileName}</div></div> },
{ key: 'tenant', title: '企业/应用', render: (record) => <div><strong>{record.tenant?.name ?? record.tenantId}</strong><div className="muted">{record.application?.name ?? '未指定应用'}</div></div> },
{ key: 'summary', title: '明细统计', render: (record) => {
const approved = record.items.filter((item) => item.status === 'approved').length;
const pending = record.items.filter((item) => item.status === 'pending_review').length;
const abnormal = record.items.filter((item) => item.status === 'invalid').length;
return `${record.items.length} · 待审${pending} · 通过${approved} · 异常${abnormal}`;
} },
{ key: 'status', title: '状态', render: (record) => <Tag tone={tone(record.status)}>{statusLabels[record.status] ?? record.status}</Tag> },
{ key: 'createdAt', title: '导入时间', render: (record) => formatDateTime(record.createdAt) },
{ key: 'actions', title: '操作', align: 'right', render: (record) => <Button icon={<Eye size={15} />} onClick={() => {
setDetail(record);
setSelected(new Set());
}} size="sm" variant="ghost"></Button> },
], []);
const columns = useMemo<Array<TableColumn<ReportImportReviewBatch>>>(
() => [
{
key: 'file',
title: '导入批次/文件',
render: (record) => (
<div>
<strong>{record.id}</strong>
<div className="muted">{record.fileName}</div>
</div>
),
},
{
key: 'tenant',
title: '企业/应用',
render: (record) => (
<div>
<strong>{record.tenant?.name ?? record.tenantId}</strong>
<div className="muted">{record.application?.name ?? '未指定应用'}</div>
</div>
),
},
{
key: 'summary',
title: '明细统计',
render: (record) => {
const approved = record.items.filter((item) => item.status === 'approved').length;
const pending = record.items.filter((item) => item.status === 'pending_review').length;
const abnormal = record.items.filter((item) => item.status === 'invalid').length;
return `${record.items.length} · 待审${pending} · 通过${approved} · 异常${abnormal}`;
},
},
{
key: 'status',
title: '状态',
render: (record) => <Tag tone={tone(record.status)}>{statusLabels[record.status] ?? record.status}</Tag>,
},
{ key: 'createdAt', title: '导入时间', render: (record) => formatDateTime(record.createdAt) },
{
key: 'actions',
title: '操作',
align: 'right',
render: (record) => (
<Button
icon={<Eye size={15} />}
onClick={() => {
setDetail(record);
setSelected(new Set());
}}
size="sm"
variant="ghost"
>
</Button>
),
},
],
[],
);
const itemColumns: Array<TableColumn<ReportImportReviewItem>> = [
{ key: 'select', title: '', width: '48px', render: (item) => <input aria-label={`选择第${item.rowNumber}`} checked={selected.has(item.id)} disabled={item.status !== 'pending_review'} onChange={() => setSelected((current) => {
const next = new Set(current);
if (next.has(item.id)) next.delete(item.id); else next.add(item.id);
return next;
})} type="checkbox" /> },
{
key: 'select',
title: '',
width: '48px',
render: (item) => (
<input
aria-label={`选择第${item.rowNumber}`}
checked={selected.has(item.id)}
disabled={item.status !== 'pending_review'}
onChange={() =>
setSelected((current) => {
const next = new Set(current);
if (next.has(item.id)) next.delete(item.id);
else next.add(item.id);
return next;
})
}
type="checkbox"
/>
),
},
{ key: 'row', title: '文件行', width: '80px', render: (item) => `${item.rowNumber}` },
{ key: 'name', title: reportType === 'signature' ? '签名' : '引流url或号码', render: (item) => <strong>{itemName(item)}</strong> },
{ key: 'operation', title: '变更类型', width: '100px', render: (item) => item.operation === 'create' ? '新增' : item.operation === 'update' ? '修改' : '无效' },
{ key: 'status', title: '状态', width: '110px', render: (item) => <Tag tone={tone(item.status)}>{itemStatusLabels[item.status] ?? item.status}</Tag> },
{
key: 'name',
title: reportType === 'signature' ? '签名' : '引流url或号码',
render: (item) => <strong>{itemName(item)}</strong>,
},
{
key: 'operation',
title: '变更类型',
width: '100px',
render: (item) => (item.operation === 'create' ? '新增' : item.operation === 'update' ? '修改' : '无效'),
},
{
key: 'status',
title: '状态',
width: '110px',
render: (item) => <Tag tone={tone(item.status)}>{itemStatusLabels[item.status] ?? item.status}</Tag>,
},
{ key: 'reason', title: '说明', render: (item) => item.errorMessage || item.reviewReason || '-' },
];
return <div className="page-stack">
{error ? <p className="form-error">{error}</p> : null}
<div className="surface audit-filter-card">
<div className="ui-filter-row">
<Input label="批次号/文件名" onChange={(event) => setKeyword(event.target.value)} placeholder="搜索导入批次或文件" value={keyword} />
<Select label="批次状态" onChange={(event) => { setStatus(event.target.value); setPage(1); }} options={[
{ label: '全部状态', value: 'all' },
{ label: '待审核', value: 'pending_review' },
{ label: '部分已审核', value: 'partially_reviewed' },
{ label: '已通过', value: 'approved' },
{ label: '部分通过', value: 'partially_approved' },
{ label: '已驳回', value: 'rejected' },
]} value={status} />
<DateRangeInput label="提交时间" onChange={(value) => { setSubmittedDateRange(value); setPage(1); }} value={submittedDateRange} />
<div className="audit-filter-actions ui-filter-actions"><Button icon={<Search size={16} />} onClick={() => { setPage(1); load(1); }}></Button><Button onClick={() => { setKeyword(''); setStatus('all'); setSubmittedDateRange({}); setPage(1); }} variant="ghost"></Button></div>
</div>
</div>
<div className="surface"><Table columns={columns} data={data.items} emptyText="暂无导入审核批次" pagination={false} rowKey="id" /></div>
<Pagination
nextDisabled={page * pageSize >= data.total}
onNext={() => setPage((current) => current + 1)}
onPageChange={setPage}
onPrevious={() => setPage((current) => Math.max(1, current - 1))}
page={page}
previousDisabled={page <= 1}
total={data.total}
totalPages={Math.max(1, Math.ceil(data.total / pageSize))}
/>
{detail ? <Modal footer={<><Button onClick={() => setDetail(undefined)} variant="ghost"></Button><Button disabled={busy || pendingItems.length === 0} icon={<X size={15} />} onClick={() => setRejectOpen(true)} variant="danger">{selected.size ? `驳回所选(${selected.size}` : `驳回全部可审核项(${pendingItems.length}`}</Button><Button disabled={busy || pendingItems.length === 0} icon={<Check size={15} />} onClick={() => void review('approve')} variant="success">{selected.size ? `通过所选(${selected.size}` : `通过全部可审核项(${pendingItems.length}`}</Button></>} onClose={() => setDetail(undefined)} open size="xl" title={`${reportType === 'signature' ? '签名' : '引流信息'}导入批次详情`}>
<div className="page-stack">
<div className="detail-grid">
<div><span></span><strong>{detail.fileName}</strong></div>
<div><span></span><strong>{detail.tenant?.name ?? detail.tenantId}</strong></div>
<div><span></span><strong>{detail.application?.name ?? '未指定应用'}</strong></div>
<div><span></span><strong>{formatDateTime(detail.createdAt)}</strong></div>
return (
<div className="page-stack">
{error ? <p className="form-error">{error}</p> : null}
<div className="surface audit-filter-card">
<div className="ui-filter-row">
<Input
label="批次号/文件名"
onChange={(event) => setKeyword(event.target.value)}
placeholder="搜索导入批次或文件"
value={keyword}
/>
<Select
label="批次状态"
onChange={(event) => {
setStatus(event.target.value);
setPage(1);
}}
options={[
{ label: '全部状态', value: 'all' },
{ label: '待审核', value: 'pending_review' },
{ label: '部分已审核', value: 'partially_reviewed' },
{ label: '已通过', value: 'approved' },
{ label: '部分通过', value: 'partially_approved' },
{ label: '已驳回', value: 'rejected' },
]}
value={status}
/>
<DateRangeInput
label="提交时间"
onChange={(value) => {
setSubmittedDateRange(value);
setPage(1);
}}
value={submittedDateRange}
/>
<div className="audit-filter-actions ui-filter-actions">
<Button
icon={<Search size={16} />}
onClick={() => {
setPage(1);
load(1);
}}
>
</Button>
<Button
onClick={() => {
setKeyword('');
setStatus('all');
setSubmittedDateRange({});
setPage(1);
}}
variant="ghost"
>
</Button>
</div>
</div>
<label className="table-actions"><input checked={allPendingSelected} onChange={() => setSelected(allPendingSelected ? new Set() : new Set(pendingItems.map((item) => item.id)))} type="checkbox" /></label>
<Table columns={itemColumns} data={detail.items} emptyText="暂无导入明细" pagination={false} rowKey="id" />
</div>
</Modal> : null}
{rejectOpen ? <Modal footer={<><Button onClick={() => setRejectOpen(false)} variant="ghost"></Button><Button disabled={busy} onClick={() => void review('reject')} variant="danger"></Button></>} onClose={() => setRejectOpen(false)} open title="驳回导入资料">
<Textarea label="驳回原因(选填)" onChange={(event) => setReason(event.target.value)} rows={4} value={reason} />
</Modal> : null}
</div>;
<div className="surface">
<Table columns={columns} data={data.items} emptyText="暂无导入审核批次" pagination={false} rowKey="id" />
</div>
<Pagination
nextDisabled={page * pageSize >= data.total}
onNext={() => setPage((current) => current + 1)}
onPageChange={setPage}
onPrevious={() => setPage((current) => Math.max(1, current - 1))}
page={page}
previousDisabled={page <= 1}
total={data.total}
totalPages={Math.max(1, Math.ceil(data.total / pageSize))}
/>
{detail ? (
<Modal
footer={
<>
<Button onClick={() => setDetail(undefined)} variant="ghost">
</Button>
<Button
disabled={busy || pendingItems.length === 0}
icon={<X size={15} />}
onClick={() => setRejectOpen(true)}
variant="danger"
>
{selected.size ? `驳回所选(${selected.size}` : `驳回全部可审核项(${pendingItems.length}`}
</Button>
<Button
disabled={busy || pendingItems.length === 0}
icon={<Check size={15} />}
onClick={() => void review('approve')}
variant="success"
>
{selected.size ? `通过所选(${selected.size}` : `通过全部可审核项(${pendingItems.length}`}
</Button>
</>
}
onClose={() => setDetail(undefined)}
open
size="xl"
title={`${reportType === 'signature' ? '签名' : '引流信息'}导入批次详情`}
>
<div className="page-stack">
<div className="detail-grid">
<div>
<span></span>
<strong>{detail.fileName}</strong>
</div>
<div>
<span></span>
<strong>{detail.tenant?.name ?? detail.tenantId}</strong>
</div>
<div>
<span></span>
<strong>{detail.application?.name ?? '未指定应用'}</strong>
</div>
<div>
<span></span>
<strong>{formatDateTime(detail.createdAt)}</strong>
</div>
</div>
<label className="table-actions">
<input
checked={allPendingSelected}
onChange={() =>
setSelected(allPendingSelected ? new Set() : new Set(pendingItems.map((item) => item.id)))
}
type="checkbox"
/>
</label>
<Table columns={itemColumns} data={detail.items} emptyText="暂无导入明细" pagination={false} rowKey="id" />
</div>
</Modal>
) : null}
{rejectOpen ? (
<Modal
footer={
<>
<Button onClick={() => setRejectOpen(false)} variant="ghost">
</Button>
<Button disabled={busy} onClick={() => void review('reject')} variant="danger">
</Button>
</>
}
onClose={() => setRejectOpen(false)}
open
title="驳回导入资料"
>
<Textarea
label="驳回原因(选填)"
onChange={(event) => setReason(event.target.value)}
rows={4}
value={reason}
/>
</Modal>
) : null}
</div>
);
}