fix: 修复报备导入映射与审核跳转
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user