feat: complete drainage review and admin search workflows

This commit is contained in:
hectorzhao
2026-07-14 09:58:18 +08:00
parent dec2430b7e
commit 6421671259
33 changed files with 1107 additions and 255 deletions
+50 -13
View File
@@ -1,32 +1,63 @@
import { useEffect, useMemo, useState } from 'react';
import { Clock3, Eye, Search } from 'lucide-react';
import { adminApi, type ReportRecord } from '@/api/adminApi';
import { Breadcrumb, Button, DateRangeInput, Input, Modal, Table, Tag, type DateRangeValue, type TableColumn } from '@/components/ui';
import { Breadcrumb, Button, DateRangeInput, Input, Modal, 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: '系统自动处理',
};
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?.siteName ?? 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.channelId}</strong></div>
<div><span></span><strong>{record.action}</strong></div>
<div><span></span><strong>{record.statusBefore ?? '-'}</strong></div>
<div><span></span><strong>{record.statusAfter ?? '-'}</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>{record.action}</strong><em>{record.reason ?? '系统记录真实报备状态变化。'}</em></div>
<div><span>{record.createdAt ?? '-'}</span><strong>{actionLabel[record.action] ?? record.action}</strong><em>{record.reason ?? `修改入口:${recordSource(record)}`}</em></div>
</section>
</div>
</Modal>
@@ -37,6 +68,7 @@ export function AdminReportRecordsPage() {
const [records, setRecords] = useState<ReportRecord[]>([]);
const [keyword, setKeyword] = useState('');
const [dateRange, setDateRange] = useState<DateRangeValue>({});
const [reportType, setReportType] = useState('all');
const [detail, setDetail] = useState<ReportRecord | null>(null);
const [error, setError] = useState('');
@@ -54,18 +86,22 @@ export function AdminReportRecordsPage() {
}, []);
const filteredRecords = useMemo(() => records.filter((record) => {
const text = `${record.taskId}${record.channelId}${record.action}${record.reason ?? ''}`;
const text = `${record.taskId}${record.channel?.name ?? ''}${record.action}${actionLabel[record.action] ?? ''}${recordSource(record)}${record.reason ?? ''}${record.task?.signature?.name ?? ''}${record.task?.signature?.purpose ?? ''}${record.task?.drainageInfo?.siteName ?? ''}${record.task?.drainageInfo?.url ?? ''}${record.task?.drainageInfo?.remark ?? ''}`;
const date = record.createdAt?.slice(0, 10) ?? '';
return (!keyword || text.includes(keyword))
&& (!dateRange.start || date >= dateRange.start)
&& (!dateRange.end || date <= dateRange.end);
}), [dateRange.end, dateRange.start, keyword, records]);
&& (!dateRange.end || date <= dateRange.end)
&& (reportType === 'all' || record.task?.reportType === reportType);
}), [dateRange.end, dateRange.start, keyword, records, reportType]);
const columns: Array<TableColumn<ReportRecord>> = [
{ key: 'task', title: '任务编号', width: '190px', render: (record) => <strong className="admin-task-id">{record.taskId}</strong> },
{ key: 'channel', title: '通道', width: '230px', render: (record) => record.channelId },
{ key: 'action', title: '动作', width: '150px', render: (record) => record.action },
{ key: 'status', title: '状态变化', width: '180px', render: (record) => <Tag tone={statusTone[record.statusAfter ?? 'pending'] ?? 'info'}>{`${record.statusBefore ?? '-'} -> ${record.statusAfter ?? '-'}`}</Tag> },
{ key: 'channel', title: '通道名称', width: '180px', render: (record) => record.channel?.name ?? '-' },
{ key: 'targetType', title: '变更主体', width: '110px', render: (record) => <Tag tone={record.task?.reportType === 'drainage' ? 'info' : 'neutral'}>{record.task?.reportType === 'drainage' ? '引流信息' : '签名'}</Tag> },
{ key: 'target', title: '主体内容', width: '260px', render: (record) => record.task?.reportType === 'drainage' ? <div className="admin-task-enterprise"><strong>{record.task?.signature?.name ?? '-'}</strong><span>{record.task?.drainageInfo?.siteName ?? '-'}</span><span>{record.task?.drainageInfo?.url ?? '-'}</span>{record.task?.drainageInfo?.remark ? <span>{record.task.drainageInfo.remark}</span> : null}</div> : <div className="admin-task-enterprise"><strong>{record.task?.signature?.name ?? '-'}</strong>{record.task?.signature?.purpose ? <span>{record.task.signature.purpose}</span> : null}</div> },
{ key: 'source', title: '修改入口', width: '150px', render: (record) => recordSource(record) },
{ key: 'action', title: '动作', width: '170px', render: (record) => actionLabel[record.action] ?? record.action },
{ key: 'status', title: '状态变化', width: '210px', render: (record) => <Tag tone={statusTone[record.statusAfter ?? 'pending'] ?? 'info'}>{`${translateStatus(record.statusBefore)}${translateStatus(record.statusAfter)}`}</Tag> },
{ key: 'time', title: '记录时间', width: '190px', render: (record) => record.createdAt ?? '-' },
{ key: 'reason', title: '备注', render: (record) => record.reason ?? '-' },
{ key: 'actions', title: '操作', align: 'right', width: '120px', render: (record) => <Button icon={<Eye size={14} />} onClick={() => setDetail(record)} size="sm" variant="ghost"></Button> },
@@ -83,10 +119,11 @@ export function AdminReportRecordsPage() {
<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} />
<DateRangeInput label="提交时间" onChange={setDateRange} value={dateRange} />
<div className="admin-task-filter__actions">
<Button icon={<Search size={16} />} onClick={loadData}></Button>
<Button onClick={() => { setKeyword(''); setDateRange({}); }} variant="ghost"></Button>
<Button onClick={() => { setKeyword(''); setDateRange({}); setReportType('all'); }} variant="ghost"></Button>
</div>
</div>