378 lines
13 KiB
TypeScript
378 lines
13 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { Clock3, Eye, Search } from 'lucide-react';
|
|
import { adminApi, type ReportRecord } from '@/api/adminApi';
|
|
import {
|
|
Breadcrumb,
|
|
Button,
|
|
DateRangeInput,
|
|
Input,
|
|
Modal,
|
|
Pagination,
|
|
Select,
|
|
Table,
|
|
Tag,
|
|
type DateRangeValue,
|
|
type TableColumn,
|
|
} from '@/components/ui';
|
|
|
|
const statusTone: Record<string, 'neutral' | 'info' | 'success' | 'warning' | 'danger'> = {
|
|
pending: 'neutral',
|
|
waiting_material: 'warning',
|
|
waiting_review: 'warning',
|
|
reporting: 'warning',
|
|
exporting: 'info',
|
|
partial: 'warning',
|
|
success: 'success',
|
|
completed: 'success',
|
|
failed: 'danger',
|
|
rejected: 'danger',
|
|
};
|
|
|
|
const statusLabel: Record<string, string> = {
|
|
pending: '待报备',
|
|
waiting_material: '待补充资料',
|
|
waiting_review: '待重新审核',
|
|
reporting: '报备中',
|
|
exporting: '导出中',
|
|
partial: '部分通过',
|
|
approved: '已通过',
|
|
success: '成功',
|
|
completed: '已完成',
|
|
failed: '失败',
|
|
rejected: '已驳回',
|
|
abandoned: '已废弃',
|
|
imported: '已导入',
|
|
deleted: '已删除',
|
|
};
|
|
|
|
const actionLabel: Record<string, string> = {
|
|
create: '创建报备任务',
|
|
manual_status_change: '人工修改状态',
|
|
export: '导出报备资料',
|
|
receipt_import: '导入回执',
|
|
audit_approved_create: '引流审核通过后创建',
|
|
audit_approved_reset: '引流审核通过后重置',
|
|
audit_resubmit_freeze: '引流修改后冻结',
|
|
audit_rejected_freeze: '引流审核驳回后冻结',
|
|
drainage_deleted: '引流信息删除',
|
|
};
|
|
|
|
const sourceEntryLabel: Record<string, string> = {
|
|
enterprise_signature: '企业签名修改',
|
|
report_task: '报备任务修改',
|
|
channel_report: '通道信息修改',
|
|
system: '系统自动处理',
|
|
legacy: '历史记录(入口未记录)',
|
|
};
|
|
|
|
function translateStatus(value?: string | null) {
|
|
if (!value) return '-';
|
|
return statusLabel[value] ?? value;
|
|
}
|
|
|
|
function recordSource(record: ReportRecord) {
|
|
if (record.sourceEntry) return sourceEntryLabel[record.sourceEntry] ?? record.sourceEntry;
|
|
return record.action === 'manual_status_change' ? '历史记录(入口未记录)' : '系统自动处理';
|
|
}
|
|
|
|
function RecordDetailModal({ record, onClose }: { record: ReportRecord; onClose: () => void }) {
|
|
const isDrainage = record.task?.reportType === 'drainage';
|
|
const target = isDrainage ? record.task?.drainageInfo?.url : record.task?.signature?.name;
|
|
return (
|
|
<Modal
|
|
footer={<Button onClick={onClose}>关闭</Button>}
|
|
onClose={onClose}
|
|
open
|
|
size="xl"
|
|
title={
|
|
<div className="template-modal-title">
|
|
<h2>报备记录详情</h2>
|
|
<p>{record.id}</p>
|
|
</div>
|
|
}
|
|
>
|
|
<div className="report-record-detail">
|
|
<div className="detail-grid">
|
|
<div>
|
|
<span>报备任务号</span>
|
|
<strong>{record.taskId}</strong>
|
|
</div>
|
|
<div>
|
|
<span>通道</span>
|
|
<strong>{record.channel?.name ?? '-'}</strong>
|
|
</div>
|
|
<div>
|
|
<span>报备类型</span>
|
|
<strong>{isDrainage ? '引流信息' : '签名'}</strong>
|
|
</div>
|
|
<div>
|
|
<span>报备对象</span>
|
|
<strong>{target ?? '-'}</strong>
|
|
</div>
|
|
<div>
|
|
<span>动作</span>
|
|
<strong>{actionLabel[record.action] ?? record.action}</strong>
|
|
</div>
|
|
<div>
|
|
<span>修改入口</span>
|
|
<strong>{recordSource(record)}</strong>
|
|
</div>
|
|
<div>
|
|
<span>状态前</span>
|
|
<strong>{translateStatus(record.statusBefore)}</strong>
|
|
</div>
|
|
<div>
|
|
<span>状态后</span>
|
|
<strong>{translateStatus(record.statusAfter)}</strong>
|
|
</div>
|
|
<div className="detail-grid__wide">
|
|
<span>失败/备注原因</span>
|
|
<strong>{record.reason ?? '-'}</strong>
|
|
</div>
|
|
</div>
|
|
<section className="report-history">
|
|
<h3>
|
|
<Clock3 size={17} />
|
|
状态历史
|
|
</h3>
|
|
<div>
|
|
<span>{record.createdAt ?? '-'}</span>
|
|
<strong>{actionLabel[record.action] ?? record.action}</strong>
|
|
<em>{record.reason ?? `修改入口:${recordSource(record)}`}</em>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
export function AdminReportRecordsPage() {
|
|
const [records, setRecords] = useState<ReportRecord[]>([]);
|
|
const [keyword, setKeyword] = useState('');
|
|
const [dateRange, setDateRange] = useState<DateRangeValue>({});
|
|
const [reportType, setReportType] = useState('all');
|
|
const [batchNo, setBatchNo] = useState('');
|
|
const [operatorKeyword, setOperatorKeyword] = useState('');
|
|
const [statusAfter, setStatusAfter] = useState('all');
|
|
const [sourceEntry, setSourceEntry] = useState('all');
|
|
const [detail, setDetail] = useState<ReportRecord | null>(null);
|
|
const [error, setError] = useState('');
|
|
const [page, setPage] = useState(1);
|
|
const [total, setTotal] = useState(0);
|
|
const [appliedFilters, setAppliedFilters] = useState({ keyword: '', dateRange: {} as DateRangeValue, reportType: 'all', batchNo: '', operatorKeyword: '', statusAfter: 'all', sourceEntry: 'all' });
|
|
const pageSize = 10;
|
|
|
|
function loadData(targetPage = page, filters = appliedFilters) {
|
|
adminApi
|
|
.listReportRecordsPage({
|
|
keyword: filters.keyword || undefined,
|
|
reportType: filters.reportType === 'all' ? undefined : (filters.reportType as 'signature' | 'drainage'),
|
|
batchNo: filters.batchNo || undefined,
|
|
operatorKeyword: filters.operatorKeyword || undefined,
|
|
statusAfter: filters.statusAfter === 'all' ? undefined : filters.statusAfter,
|
|
sourceEntry: filters.sourceEntry === 'all' ? undefined : filters.sourceEntry,
|
|
createdAtFrom: filters.dateRange.start || undefined,
|
|
createdAtTo: filters.dateRange.end || undefined,
|
|
page: targetPage,
|
|
pageSize,
|
|
})
|
|
.then((result) => {
|
|
setRecords(result.items);
|
|
setTotal(result.total);
|
|
setError('');
|
|
})
|
|
.catch((failure: Error) => setError(failure.message || '报备记录加载失败'));
|
|
}
|
|
|
|
useEffect(() => {
|
|
loadData(page);
|
|
}, [page]);
|
|
|
|
const columns: Array<TableColumn<ReportRecord>> = [
|
|
{
|
|
key: 'task',
|
|
title: '报备任务号',
|
|
width: '160px',
|
|
render: (record) => <strong className="admin-task-id admin-report-record-id" title={record.taskId}>{record.taskId}</strong>,
|
|
},
|
|
{ key: 'channel', title: '通道名称', width: '130px', render: (record) => record.channel?.name ?? '-' },
|
|
{
|
|
key: 'target',
|
|
title: '变更对象',
|
|
width: '230px',
|
|
render: (record) =>
|
|
record.task?.reportType === 'drainage' ? (
|
|
<div className="admin-task-enterprise admin-report-record-target">
|
|
<Tag tone="info">引流信息</Tag>
|
|
<strong>{record.task?.signature?.name ?? '-'}</strong>
|
|
<span>{record.task?.drainageInfo?.url ?? '-'}</span>
|
|
</div>
|
|
) : (
|
|
<div className="admin-task-enterprise admin-report-record-target">
|
|
<Tag tone="neutral">签名</Tag>
|
|
<strong>{record.task?.signature?.name ?? '-'}</strong>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
key: 'action',
|
|
title: '变更动作',
|
|
width: '180px',
|
|
render: (record) => (
|
|
<div className="admin-report-record-meta">
|
|
<strong>{actionLabel[record.action] ?? record.action}</strong>
|
|
<span>{recordSource(record)}</span>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
key: 'status',
|
|
title: '状态变化',
|
|
width: '150px',
|
|
render: (record) => (
|
|
<Tag
|
|
tone={statusTone[record.statusAfter ?? 'pending'] ?? 'info'}
|
|
>{`${translateStatus(record.statusBefore)} → ${translateStatus(record.statusAfter)}`}</Tag>
|
|
),
|
|
},
|
|
{
|
|
key: 'operatorTime',
|
|
title: '操作人 / 时间',
|
|
width: '175px',
|
|
render: (record) => (
|
|
<div className="admin-report-record-meta">
|
|
<strong>{record.operator?.displayName ?? record.operator?.username ?? '系统'}</strong>
|
|
<span>{record.createdAt ?? '-'}</span>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
key: 'actions',
|
|
title: '操作',
|
|
align: 'right',
|
|
width: '76px',
|
|
render: (record) => (
|
|
<Button icon={<Eye size={14} />} onClick={() => setDetail(record)} size="sm" variant="ghost">
|
|
详情
|
|
</Button>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<section className="page-stack admin-sms-task-page report-record-page">
|
|
<div className="page-heading">
|
|
<div>
|
|
<Breadcrumb items={['报备工作台', '状态记录']} />
|
|
<h1>状态记录</h1>
|
|
</div>
|
|
</div>
|
|
{error ? <p className="form-error">{error}</p> : null}
|
|
|
|
<div className="surface admin-task-filter">
|
|
<Input
|
|
label="报备任务号/通道/动作/备注"
|
|
onChange={(event) => setKeyword(event.target.value)}
|
|
placeholder="请输入报备任务号、通道、动作或备注"
|
|
value={keyword}
|
|
/>
|
|
<Select
|
|
label="报备类型"
|
|
onChange={(event) => setReportType(event.target.value)}
|
|
options={[
|
|
{ label: '全部类型', value: 'all' },
|
|
{ label: '签名报备', value: 'signature' },
|
|
{ label: '引流信息报备', value: 'drainage' },
|
|
]}
|
|
value={reportType}
|
|
/>
|
|
<Input
|
|
label="报备批次号"
|
|
onChange={(event) => setBatchNo(event.target.value)}
|
|
placeholder="输入批次号"
|
|
value={batchNo}
|
|
/>
|
|
<Input
|
|
label="操作人"
|
|
onChange={(event) => setOperatorKeyword(event.target.value)}
|
|
placeholder="姓名或账号"
|
|
value={operatorKeyword}
|
|
/>
|
|
<Select
|
|
label="变更后状态"
|
|
onChange={(event) => setStatusAfter(event.target.value)}
|
|
options={[
|
|
{ label: '全部状态', value: 'all' },
|
|
{ label: '未报备', value: 'pending' },
|
|
{ label: '报备中', value: 'reporting' },
|
|
{ label: '报备通过', value: 'approved' },
|
|
{ label: '报备失败', value: 'failed' },
|
|
{ label: '已放弃', value: 'abandoned' },
|
|
]}
|
|
value={statusAfter}
|
|
/>
|
|
<Select
|
|
label="修改入口"
|
|
onChange={(event) => setSourceEntry(event.target.value)}
|
|
options={[
|
|
{ label: '全部入口', value: 'all' },
|
|
{ label: '企业签名修改', value: 'enterprise_signature' },
|
|
{ label: '通道报备明细', value: 'report_task' },
|
|
{ label: '通道详情', value: 'channel_report' },
|
|
{ label: '系统自动处理', value: 'system' },
|
|
]}
|
|
value={sourceEntry}
|
|
/>
|
|
<DateRangeInput label="提交时间" onChange={setDateRange} value={dateRange} />
|
|
<div className="admin-task-filter__actions">
|
|
<Button
|
|
icon={<Search size={16} />}
|
|
onClick={() => {
|
|
const filters = { keyword: keyword.trim(), dateRange, reportType, batchNo: batchNo.trim(), operatorKeyword: operatorKeyword.trim(), statusAfter, sourceEntry };
|
|
setAppliedFilters(filters);
|
|
if (page !== 1) setPage(1);
|
|
else loadData(1, filters);
|
|
}}
|
|
>
|
|
查询
|
|
</Button>
|
|
<Button
|
|
onClick={() => {
|
|
setKeyword('');
|
|
setDateRange({});
|
|
setReportType('all');
|
|
setBatchNo('');
|
|
setOperatorKeyword('');
|
|
setStatusAfter('all');
|
|
setSourceEntry('all');
|
|
const filters = { keyword: '', dateRange: {} as DateRangeValue, reportType: 'all', batchNo: '', operatorKeyword: '', statusAfter: 'all', sourceEntry: 'all' };
|
|
setAppliedFilters(filters);
|
|
if (page !== 1) setPage(1);
|
|
else loadData(1, filters);
|
|
}}
|
|
variant="ghost"
|
|
>
|
|
重置
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="surface admin-task-table-card report-task-table-card">
|
|
<Table columns={columns} data={records} emptyText="暂无报备记录" pagination={false} rowKey="id" />
|
|
</div>
|
|
<Pagination
|
|
nextDisabled={page * pageSize >= total}
|
|
onNext={() => setPage((current) => current + 1)}
|
|
onPageChange={setPage}
|
|
onPrevious={() => setPage((current) => Math.max(1, current - 1))}
|
|
page={page}
|
|
previousDisabled={page <= 1}
|
|
total={total}
|
|
totalPages={Math.max(1, Math.ceil(total / pageSize))}
|
|
/>
|
|
|
|
{detail ? <RecordDetailModal onClose={() => setDetail(null)} record={detail} /> : null}
|
|
</section>
|
|
);
|
|
}
|