148 lines
7.2 KiB
TypeScript
148 lines
7.2 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
|
import { Download, Eye, FileUp, Search } from 'lucide-react';
|
|
import { adminApi, type ReportTask } from '@/api/adminApi';
|
|
import { Breadcrumb, Button, DateRangeInput, Input, Modal, Table, Tag, Textarea, type DateRangeValue, type TableColumn } from '@/components/ui';
|
|
|
|
const statusMeta: Record<string, { label: string; tone: 'neutral' | 'info' | 'success' | 'warning' | 'danger' }> = {
|
|
pending: { label: '待处理', tone: 'neutral' },
|
|
ready: { label: '待导出', tone: 'info' },
|
|
exported: { label: '已导出', tone: 'warning' },
|
|
reporting: { label: '报备中', tone: 'warning' },
|
|
partial: { label: '部分完成', tone: 'info' },
|
|
completed: { label: '已完成', tone: 'success' },
|
|
failed: { label: '有失败', tone: 'danger' },
|
|
};
|
|
|
|
function ReceiptImportModal({ onClose, onSubmit }: { onClose: () => void; onSubmit: (fileName: string, remark: string) => void }) {
|
|
const [fileName, setFileName] = useState('');
|
|
const [remark, setRemark] = useState('');
|
|
return (
|
|
<Modal
|
|
footer={<><Button onClick={onClose} variant="ghost">取消</Button><Button disabled={!fileName} onClick={() => onSubmit(fileName, remark)}>确认导入</Button></>}
|
|
onClose={onClose}
|
|
open
|
|
size="xl"
|
|
title={<div className="template-modal-title"><h2>导入报备回执</h2><p>上传运营商回执并记录状态。</p></div>}
|
|
>
|
|
<div className="report-receipt-modal">
|
|
<label className="report-upload-drop">
|
|
<FileUp size={38} />
|
|
<strong>{fileName || '选择回执文件'}</strong>
|
|
<span>支持 Excel、CSV、PDF 等真实回执文件。</span>
|
|
<input onChange={(event) => setFileName(event.target.files?.[0]?.name ?? '')} style={{ display: 'none' }} type="file" />
|
|
</label>
|
|
<Textarea label="导入备注" onChange={(event) => setRemark(event.target.value)} placeholder="记录回执来源、运营商工单号或人工处理说明" rows={4} value={remark} />
|
|
</div>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
function TaskDetailModal({ task, onClose }: { task: ReportTask; onClose: () => void }) {
|
|
const status = statusMeta[task.status] ?? { label: task.status, tone: 'info' as const };
|
|
return (
|
|
<Modal footer={<Button onClick={onClose}>关闭</Button>} onClose={onClose} open size="xl" title={<div className="template-modal-title"><h2>报备任务详情</h2><p>{task.id}</p></div>}>
|
|
<div className="admin-task-detail report-task-detail">
|
|
<div className="admin-task-metrics">
|
|
<div className="admin-task-metric"><span>签名</span><strong>{task.signature?.name ?? task.signatureId}</strong></div>
|
|
<div className="admin-task-metric admin-task-metric--primary"><span>通道</span><strong>{task.channel?.name ?? task.channelId}</strong></div>
|
|
<div className="admin-task-metric admin-task-metric--success"><span>当前状态</span><strong>{status.label}</strong></div>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
export function AdminReportTasksPage() {
|
|
const [tasks, setTasks] = useState<ReportTask[]>([]);
|
|
const [keyword, setKeyword] = useState('');
|
|
const [dateRange, setDateRange] = useState<DateRangeValue>({});
|
|
const [receiptTask, setReceiptTask] = useState<ReportTask | null>(null);
|
|
const [detailTask, setDetailTask] = useState<ReportTask | null>(null);
|
|
const [error, setError] = useState('');
|
|
|
|
function loadData() {
|
|
adminApi.listReportTasks()
|
|
.then((items) => {
|
|
setTasks(items);
|
|
setError('');
|
|
})
|
|
.catch((failure: Error) => setError(failure.message || '报备任务加载失败'));
|
|
}
|
|
|
|
useEffect(() => {
|
|
loadData();
|
|
}, []);
|
|
|
|
const filteredTasks = useMemo(() => tasks.filter((task) => {
|
|
const text = `${task.id}${task.channel?.name ?? task.channelId}${task.signature?.name ?? task.signatureId}`;
|
|
const date = task.createdAt?.slice(0, 10) ?? '';
|
|
return (!keyword || text.includes(keyword))
|
|
&& (!dateRange.start || date >= dateRange.start)
|
|
&& (!dateRange.end || date <= dateRange.end);
|
|
}), [dateRange.end, dateRange.start, keyword, tasks]);
|
|
|
|
function exportTask(task: ReportTask) {
|
|
adminApi.createReportExport(task.id, { fileName: `${task.id}.xlsx`, rowCount: 0 })
|
|
.then(loadData)
|
|
.catch((failure: Error) => setError(failure.message || '报备任务导出失败'));
|
|
}
|
|
|
|
function importReceipt(fileName: string, remark: string) {
|
|
if (!receiptTask) return;
|
|
adminApi.importReportReceipt(receiptTask.id, { fileName, reason: remark, statusAfter: 'partial' })
|
|
.then(() => {
|
|
setReceiptTask(null);
|
|
loadData();
|
|
})
|
|
.catch((failure: Error) => setError(failure.message));
|
|
}
|
|
|
|
const columns: Array<TableColumn<ReportTask>> = [
|
|
{ key: 'id', title: '任务编号', width: '190px', render: (record) => <strong className="admin-task-id">{record.id}</strong> },
|
|
{ key: 'scope', title: '通道/签名', width: '280px', render: (record) => <div className="admin-task-enterprise"><strong>{record.channel?.name ?? record.channelId}</strong><span>{record.signature?.name ?? record.signatureId}</span></div> },
|
|
{ key: 'status', title: '状态', width: '110px', render: (record) => <Tag tone={(statusMeta[record.status] ?? { tone: 'info' as const }).tone}>{(statusMeta[record.status] ?? { label: record.status }).label}</Tag> },
|
|
{ key: 'time', title: '创建时间', width: '190px', render: (record) => record.createdAt ?? '-' },
|
|
{
|
|
key: 'actions',
|
|
title: '操作',
|
|
align: 'right',
|
|
width: '240px',
|
|
render: (record) => (
|
|
<div className="admin-task-actions">
|
|
<Button icon={<Eye size={14} />} onClick={() => setDetailTask(record)} size="sm" variant="ghost">详情</Button>
|
|
<Button icon={<Download size={14} />} onClick={() => exportTask(record)} size="sm" variant="ghost">生成同范围任务</Button>
|
|
<Button icon={<FileUp size={14} />} onClick={() => setReceiptTask(record)} size="sm" variant="ghost">导入回执</Button>
|
|
</div>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<section className="page-stack admin-sms-task-page report-task-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} />
|
|
<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>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="surface admin-task-table-card report-task-table-card">
|
|
<Table columns={columns} data={filteredTasks} emptyText="暂无报备任务" rowKey="id" />
|
|
</div>
|
|
|
|
{receiptTask ? <ReceiptImportModal onClose={() => setReceiptTask(null)} onSubmit={importReceipt} /> : null}
|
|
{detailTask ? <TaskDetailModal onClose={() => setDetailTask(null)} task={detailTask} /> : null}
|
|
</section>
|
|
);
|
|
}
|