fix: connect remaining sms pages to real backend
This commit is contained in:
@@ -1,27 +1,10 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Download, Eye, FileUp, Plus, Search, Settings2 } from 'lucide-react';
|
||||
import { Breadcrumb, Button, DateRangeInput, Input, Modal, Select, Table, Tag, Textarea, type DateRangeValue, type TableColumn } from '@/components/ui';
|
||||
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';
|
||||
|
||||
type ReportTaskStatus = 'draft' | 'ready' | 'exported' | 'reporting' | 'partial' | 'completed' | 'failed';
|
||||
|
||||
type ReportTask = {
|
||||
id: string;
|
||||
channel: string;
|
||||
carrier: '移动' | '联通' | '电信';
|
||||
enterprise: string;
|
||||
signatureCount: number;
|
||||
drainageCount: number;
|
||||
missingCount: number;
|
||||
status: ReportTaskStatus;
|
||||
createdAt: string;
|
||||
exportedAt?: string;
|
||||
receiptAt?: string;
|
||||
owner: string;
|
||||
remark: string;
|
||||
};
|
||||
|
||||
const statusMeta: Record<ReportTaskStatus, { label: string; tone: 'neutral' | 'info' | 'success' | 'warning' | 'danger' }> = {
|
||||
draft: { label: '待生成', tone: 'neutral' },
|
||||
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' },
|
||||
@@ -30,246 +13,104 @@ const statusMeta: Record<ReportTaskStatus, { label: string; tone: 'neutral' | 'i
|
||||
failed: { label: '有失败', tone: 'danger' },
|
||||
};
|
||||
|
||||
const carrierOptions = [
|
||||
{ label: '全部运营商', value: 'all' },
|
||||
{ label: '移动', value: '移动' },
|
||||
{ label: '联通', value: '联通' },
|
||||
{ label: '电信', value: '电信' },
|
||||
];
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '全部状态', value: 'all' },
|
||||
{ label: '待生成', value: 'draft' },
|
||||
{ label: '待导出', value: 'ready' },
|
||||
{ label: '已导出', value: 'exported' },
|
||||
{ label: '报备中', value: 'reporting' },
|
||||
{ label: '部分完成', value: 'partial' },
|
||||
{ label: '已完成', value: 'completed' },
|
||||
{ label: '有失败', value: 'failed' },
|
||||
];
|
||||
|
||||
const initialTasks: ReportTask[] = [
|
||||
{
|
||||
id: 'RPT-TASK-20260630-001',
|
||||
channel: '行北-集市三甲医院-39',
|
||||
carrier: '移动',
|
||||
enterprise: '上海XXXXX科技有限公司',
|
||||
signatureCount: 18,
|
||||
drainageCount: 9,
|
||||
missingCount: 0,
|
||||
status: 'ready',
|
||||
createdAt: '2026-06-30 09:12:00',
|
||||
owner: '运营',
|
||||
remark: '字段版本 V3,等待导出提交运营商。',
|
||||
},
|
||||
{
|
||||
id: 'RPT-TASK-20260629-006',
|
||||
channel: '联通-行政-杭州甲医院-37',
|
||||
carrier: '联通',
|
||||
enterprise: '重庆进载数智',
|
||||
signatureCount: 12,
|
||||
drainageCount: 4,
|
||||
missingCount: 2,
|
||||
status: 'exported',
|
||||
createdAt: '2026-06-29 15:24:00',
|
||||
exportedAt: '2026-06-29 16:02:11',
|
||||
owner: '李青',
|
||||
remark: '2 条引流链接缺 ICP 备案截图。',
|
||||
},
|
||||
{
|
||||
id: 'RPT-TASK-20260628-003',
|
||||
channel: '移动-行北-上海甲医院-38',
|
||||
carrier: '移动',
|
||||
enterprise: '超感世纪互三网',
|
||||
signatureCount: 7,
|
||||
drainageCount: 3,
|
||||
missingCount: 0,
|
||||
status: 'completed',
|
||||
createdAt: '2026-06-28 11:36:00',
|
||||
exportedAt: '2026-06-28 12:20:16',
|
||||
receiptAt: '2026-06-29 10:05:44',
|
||||
owner: '运营',
|
||||
remark: '已导入移动回执,全部通过。',
|
||||
},
|
||||
];
|
||||
|
||||
function RemarkCell({ value }: { value?: string }) {
|
||||
return (
|
||||
<div className={['admin-remark-cell', value ? '' : 'admin-remark-cell--empty'].filter(Boolean).join(' ')}>
|
||||
{value || '暂无备注'}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusTag({ status }: { status: ReportTaskStatus }) {
|
||||
return <Tag tone={statusMeta[status].tone}>{statusMeta[status].label}</Tag>;
|
||||
}
|
||||
|
||||
function ReceiptImportModal({ onClose, onSubmit }: { onClose: () => void; onSubmit: () => void }) {
|
||||
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 onClick={onSubmit}>确认导入</Button></>}
|
||||
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>}
|
||||
title={<div className="template-modal-title"><h2>导入报备回执</h2><p>上传运营商回执并记录状态。</p></div>}
|
||||
>
|
||||
<div className="report-receipt-modal">
|
||||
<div className="report-upload-drop">
|
||||
<label className="report-upload-drop">
|
||||
<FileUp size={38} />
|
||||
<strong>选择回执文件</strong>
|
||||
<span>支持 Excel、CSV,字段包含签名、引流信息、通道状态和驳回原因。</span>
|
||||
</div>
|
||||
<div className="report-receipt-preview">
|
||||
<h3>导入预览</h3>
|
||||
<div><span>匹配成功</span><strong>26 条</strong></div>
|
||||
<div><span>待人工确认</span><strong>3 条</strong></div>
|
||||
<div><span>未匹配</span><strong>1 条</strong></div>
|
||||
</div>
|
||||
<Textarea label="导入备注" placeholder="记录回执来源、运营商工单号或人工处理说明" rows={4} />
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function GenerateTaskModal({ onClose, onSubmit }: { onClose: () => void; onSubmit: (task: ReportTask) => void }) {
|
||||
const [channel, setChannel] = useState('行北-集市三甲医院-39');
|
||||
const [carrier, setCarrier] = useState<'移动' | '联通' | '电信'>('移动');
|
||||
const [enterprise, setEnterprise] = useState('全部资料完整企业');
|
||||
|
||||
function submit() {
|
||||
onSubmit({
|
||||
id: `RPT-TASK-${Date.now()}`,
|
||||
channel,
|
||||
carrier,
|
||||
enterprise,
|
||||
signatureCount: 15,
|
||||
drainageCount: 6,
|
||||
missingCount: enterprise === '全部资料完整企业' ? 0 : 2,
|
||||
status: enterprise === '全部资料完整企业' ? 'ready' : 'draft',
|
||||
createdAt: '2026-06-30 10:00:00',
|
||||
owner: '运营',
|
||||
remark: '由候选签名池生成,按当前通道字段版本校验。',
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
footer={<><Button onClick={onClose} variant="ghost">取消</Button><Button onClick={submit}>生成任务</Button></>}
|
||||
onClose={onClose}
|
||||
open
|
||||
size="xl"
|
||||
title={<div className="template-modal-title"><h2>生成通道签名报备任务</h2><p>按通道字段配置筛选资料完整的签名和引流信息。</p></div>}
|
||||
>
|
||||
<div className="report-task-generate">
|
||||
<section>
|
||||
<h3>任务范围</h3>
|
||||
<div className="admin-system-modal-form">
|
||||
<Select label="报备通道" onChange={(event) => setChannel(event.target.value)} options={[
|
||||
{ label: '行北-集市三甲医院-39', value: '行北-集市三甲医院-39' },
|
||||
{ label: '联通-行政-杭州甲医院-37', value: '联通-行政-杭州甲医院-37' },
|
||||
{ label: '移动-行北-上海甲医院-38', value: '移动-行北-上海甲医院-38' },
|
||||
]} value={channel} />
|
||||
<Select label="运营商" onChange={(event) => setCarrier(event.target.value as '移动' | '联通' | '电信')} options={carrierOptions.filter((item) => item.value !== 'all')} value={carrier} />
|
||||
<Select label="候选企业" onChange={(event) => setEnterprise(event.target.value)} options={[
|
||||
{ label: '全部资料完整企业', value: '全部资料完整企业' },
|
||||
{ label: '上海XXXXX科技有限公司', value: '上海XXXXX科技有限公司' },
|
||||
{ label: '重庆进载数智', value: '重庆进载数智' },
|
||||
]} value={enterprise} />
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h3>生成预览</h3>
|
||||
<div className="report-task-preview">
|
||||
<div><span>可生成签名</span><strong>15</strong></div>
|
||||
<div><span>可生成引流信息</span><strong>6</strong></div>
|
||||
<div><span>缺资料</span><strong>{enterprise === '全部资料完整企业' ? 0 : 2}</strong></div>
|
||||
<div><span>导出模板</span><strong>通道字段版本 V3</strong></div>
|
||||
</div>
|
||||
</section>
|
||||
<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.signatureCount}</strong></div>
|
||||
<div className="admin-task-metric admin-task-metric--primary"><span>引流信息</span><strong>{task.drainageCount}</strong></div>
|
||||
<div className="admin-task-metric"><span>缺资料</span><strong>{task.missingCount}</strong></div>
|
||||
<div className="admin-task-metric admin-task-metric--success"><span>当前状态</span><strong>{statusMeta[task.status].label}</strong></div>
|
||||
<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>
|
||||
<section className="admin-task-card">
|
||||
<h3><Settings2 size={18} />任务信息</h3>
|
||||
<dl className="admin-task-info-list">
|
||||
<div><dt>通道</dt><dd>{task.channel}</dd></div>
|
||||
<div><dt>运营商</dt><dd>{task.carrier}</dd></div>
|
||||
<div><dt>企业范围</dt><dd>{task.enterprise}</dd></div>
|
||||
<div><dt>创建时间</dt><dd>{task.createdAt}</dd></div>
|
||||
<div><dt>导出时间</dt><dd>{task.exportedAt ?? '-'}</dd></div>
|
||||
<div><dt>回执导入</dt><dd>{task.receiptAt ?? '-'}</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export function AdminReportTasksPage() {
|
||||
const [tasks, setTasks] = useState(initialTasks);
|
||||
const [tasks, setTasks] = useState<ReportTask[]>([]);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [carrier, setCarrier] = useState('all');
|
||||
const [status, setStatus] = useState('all');
|
||||
const [dateRange, setDateRange] = useState<DateRangeValue>({});
|
||||
const [generateOpen, setGenerateOpen] = useState(false);
|
||||
const [receiptTask, setReceiptTask] = useState<ReportTask | 'batch' | null>(null);
|
||||
const [receiptTask, setReceiptTask] = useState<ReportTask | null>(null);
|
||||
const [detailTask, setDetailTask] = useState<ReportTask | null>(null);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const filteredTasks = useMemo(() => tasks.filter((task) => {
|
||||
const text = `${task.id}${task.channel}${task.enterprise}`;
|
||||
const date = task.createdAt.slice(0, 10);
|
||||
return (!keyword || text.includes(keyword))
|
||||
&& (carrier === 'all' || task.carrier === carrier)
|
||||
&& (status === 'all' || task.status === status)
|
||||
&& (!dateRange.start || date >= dateRange.start)
|
||||
&& (!dateRange.end || date <= dateRange.end);
|
||||
}), [carrier, dateRange.end, dateRange.start, keyword, status, tasks]);
|
||||
|
||||
function exportTask(taskId: string) {
|
||||
setTasks((current) => current.map((task) => (
|
||||
task.id === taskId ? { ...task, status: 'exported', exportedAt: '2026-06-30 10:18:00' } : task
|
||||
)));
|
||||
function loadData() {
|
||||
adminApi.listReportTasks()
|
||||
.then((items) => {
|
||||
setTasks(items);
|
||||
setError('');
|
||||
})
|
||||
.catch((failure: Error) => setError(failure.message || '报备任务加载失败'));
|
||||
}
|
||||
|
||||
function importReceipt() {
|
||||
if (receiptTask && receiptTask !== 'batch') {
|
||||
setTasks((current) => current.map((task) => (
|
||||
task.id === receiptTask.id ? { ...task, status: 'partial', receiptAt: '2026-06-30 10:36:00' } : task
|
||||
)));
|
||||
}
|
||||
setReceiptTask(null);
|
||||
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}</strong><span>{record.enterprise}</span></div> },
|
||||
{ key: 'carrier', title: '运营商', width: '90px', render: (record) => <Tag tone="info">{record.carrier}</Tag> },
|
||||
{ key: 'counts', title: '资料数量', width: '190px', render: (record) => <div className="admin-task-counts"><span>签名 {record.signatureCount}</span><span>引流 {record.drainageCount}</span><strong>缺 {record.missingCount}</strong></div> },
|
||||
{ key: 'status', title: '状态', width: '110px', render: (record) => <StatusTag status={record.status} /> },
|
||||
{ key: 'time', title: '流转时间', width: '210px', render: (record) => <div className="report-task-time"><span>创建 {record.createdAt}</span><span>导出 {record.exportedAt ?? '-'}</span><span>回执 {record.receiptAt ?? '-'}</span></div> },
|
||||
{ key: 'remark', title: '备注', width: '300px', render: (record) => <RemarkCell value={record.remark} /> },
|
||||
{ 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: '280px',
|
||||
width: '240px',
|
||||
render: (record) => (
|
||||
<div className="admin-task-actions">
|
||||
<Button icon={<Eye size={14} />} onClick={() => setDetailTask(record)} size="sm" variant="ghost">详情</Button>
|
||||
<Button disabled={record.missingCount > 0} icon={<Download size={14} />} onClick={() => exportTask(record.id)} 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>
|
||||
),
|
||||
@@ -280,23 +121,18 @@ export function AdminReportTasksPage() {
|
||||
<section className="page-stack admin-sms-task-page report-task-page">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<Breadcrumb items={['报备任务', '报备任务']} />
|
||||
<h1>报备任务</h1>
|
||||
</div>
|
||||
<div className="report-page-actions">
|
||||
<Button icon={<FileUp size={16} />} onClick={() => setReceiptTask('batch')} variant="secondary">批量导入回执</Button>
|
||||
<Button icon={<Plus size={16} />} onClick={() => setGenerateOpen(true)}>生成报备任务</Button>
|
||||
<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) => setCarrier(event.target.value)} options={carrierOptions} value={carrier} />
|
||||
<Select label="任务状态" onChange={(event) => setStatus(event.target.value)} options={statusOptions} value={status} />
|
||||
<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} />}>查询</Button>
|
||||
<Button onClick={() => { setKeyword(''); setCarrier('all'); setStatus('all'); setDateRange({}); }} variant="ghost">重置</Button>
|
||||
<Button icon={<Search size={16} />} onClick={loadData}>查询</Button>
|
||||
<Button onClick={() => { setKeyword(''); setDateRange({}); }} variant="ghost">重置</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -304,7 +140,6 @@ export function AdminReportTasksPage() {
|
||||
<Table columns={columns} data={filteredTasks} emptyText="暂无报备任务" rowKey="id" />
|
||||
</div>
|
||||
|
||||
{generateOpen ? <GenerateTaskModal onClose={() => setGenerateOpen(false)} onSubmit={(task) => { setTasks((current) => [task, ...current]); setGenerateOpen(false); }} /> : null}
|
||||
{receiptTask ? <ReceiptImportModal onClose={() => setReceiptTask(null)} onSubmit={importReceipt} /> : null}
|
||||
{detailTask ? <TaskDetailModal onClose={() => setDetailTask(null)} task={detailTask} /> : null}
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user