feat: build reporting workbench workflow
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Eye, Search } from 'lucide-react';
|
||||
import { adminApi, fileDownloadUrl, type ReportTask } from '@/api/adminApi';
|
||||
import { Download, Eye, Search } from 'lucide-react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { adminApi, fileDownloadUrl, type ReportTask, type SingleReportMaterialDetail } from '@/api/adminApi';
|
||||
import { Breadcrumb, Button, CarrierTag, DateRangeInput, Input, Modal, Pagination, Select, Table, Tag, Textarea, type DateRangeValue, type TableColumn } from '@/components/ui';
|
||||
import { formatDateTime } from '@/utils/dateTime';
|
||||
|
||||
@@ -31,45 +32,122 @@ function taskTargetLabel(task: ReportTask) {
|
||||
function TaskDetailModal({ task, onClose }: { task: ReportTask; onClose: () => void }) {
|
||||
const status = statusMeta[task.status] ?? { label: task.status, tone: 'info' as const };
|
||||
const source = task.exportItems?.[0];
|
||||
return <Modal footer={<Button onClick={onClose}>关闭</Button>} onClose={onClose} open size="xl" title="报备明细详情">
|
||||
<div className="page-stack">
|
||||
<div className="detail-grid">
|
||||
<div><span>报备对象</span><strong>{taskTargetLabel(task)}</strong></div>
|
||||
<div><span>资料类型</span><strong>{task.reportType === 'drainage' ? '引流信息' : '签名'}</strong></div>
|
||||
<div><span>企业</span><strong>{task.signature?.tenant?.name ?? task.tenantId}</strong></div>
|
||||
<div><span>企业应用</span><strong>{task.signature?.application?.name ?? '未指定应用'}</strong></div>
|
||||
<div><span>通道</span><strong>{task.channel?.name ?? task.channelId}</strong></div>
|
||||
{task.reportType !== 'drainage' ? <div><span>运营商</span>{task.carrier ? <CarrierTag carrier={task.carrier} /> : <strong>历史通道级(未拆分)</strong>}</div> : null}
|
||||
{task.reportType !== 'drainage' ? <div><span>当前通过时间</span><strong>{task.approvedAt ? formatDateTime(task.approvedAt) : '-'}</strong></div> : null}
|
||||
<div><span>当前状态</span><Tag tone={status.tone}>{status.label}</Tag></div>
|
||||
<div><span>创建时间</span><strong>{formatDateTime(task.createdAt)}</strong></div>
|
||||
<div><span>最后更新时间</span><strong>{formatDateTime(task.updatedAt)}</strong></div>
|
||||
<div><span>资料版本</span><strong>{source ? `V${source.batchItem.materialVersion}` : '-'}</strong></div>
|
||||
<div><span>所属批次</span><strong>{source?.batchItem.batch.batchNo ?? '-'}</strong></div>
|
||||
<div><span>报备文件行</span><strong>{source ? `第${source.rowNumber}行` : '-'}</strong></div>
|
||||
<div><span>当前说明</span><strong>{task.reason || '-'}</strong></div>
|
||||
</div>
|
||||
{source?.exportFile.fileObjectId ? <div className="surface" style={{ padding: 16 }}><a href={fileDownloadUrl(source.exportFile.fileObjectId)}>下载报备文件:{source.exportFile.fileName}</a></div> : null}
|
||||
<div className="surface" style={{ padding: 16 }}>
|
||||
<h3>状态记录</h3>
|
||||
<div className="page-stack" style={{ marginTop: 12 }}>
|
||||
{(task.records ?? []).length ? task.records!.map((record) => <div className="detail-grid" key={record.id}>
|
||||
<div><span>时间</span><strong>{formatDateTime(record.createdAt)}</strong></div>
|
||||
<div><span>动作</span><strong>{actionLabels[record.action] ?? record.action}</strong></div>
|
||||
<div><span>状态变化</span><strong>{statusMeta[record.statusBefore ?? '']?.label ?? record.statusBefore ?? '-'} → {statusMeta[record.statusAfter]?.label ?? record.statusAfter}</strong></div>
|
||||
<div><span>说明</span><strong>{record.reason || '-'}</strong></div>
|
||||
</div>) : <p className="muted">暂无状态记录</p>}
|
||||
return (
|
||||
<Modal footer={<Button onClick={onClose}>关闭</Button>} onClose={onClose} open size="xl" title="报备明细详情">
|
||||
<div className="page-stack">
|
||||
<div className="detail-grid">
|
||||
<div>
|
||||
<span>报备对象</span>
|
||||
<strong>{taskTargetLabel(task)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>资料类型</span>
|
||||
<strong>{task.reportType === 'drainage' ? '引流信息' : '签名'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>企业</span>
|
||||
<strong>{task.signature?.tenant?.name ?? task.tenantId}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>企业应用</span>
|
||||
<strong>{task.signature?.application?.name ?? '未指定应用'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>通道</span>
|
||||
<strong>{task.channel?.name ?? task.channelId}</strong>
|
||||
</div>
|
||||
{task.reportType !== 'drainage' ? (
|
||||
<div>
|
||||
<span>运营商</span>
|
||||
{task.carrier ? <CarrierTag carrier={task.carrier} /> : <strong>历史通道级(未拆分)</strong>}
|
||||
</div>
|
||||
) : null}
|
||||
{task.reportType !== 'drainage' ? (
|
||||
<div>
|
||||
<span>当前通过时间</span>
|
||||
<strong>{task.approvedAt ? formatDateTime(task.approvedAt) : '-'}</strong>
|
||||
</div>
|
||||
) : null}
|
||||
<div>
|
||||
<span>当前状态</span>
|
||||
<Tag tone={status.tone}>{status.label}</Tag>
|
||||
</div>
|
||||
<div>
|
||||
<span>创建时间</span>
|
||||
<strong>{formatDateTime(task.createdAt)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>最后更新时间</span>
|
||||
<strong>{formatDateTime(task.updatedAt)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>资料版本</span>
|
||||
<strong>{source ? `V${source.batchItem.materialVersion}` : '-'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>所属批次</span>
|
||||
<strong>{source?.batchItem.batch.batchNo ?? '-'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>报备文件行</span>
|
||||
<strong>{source ? `第${source.rowNumber}行` : '-'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>当前说明</span>
|
||||
<strong>{task.reason || '-'}</strong>
|
||||
</div>
|
||||
</div>
|
||||
{source?.exportFile.fileObjectId ? (
|
||||
<div className="surface" style={{ padding: 16 }}>
|
||||
<a href={fileDownloadUrl(source.exportFile.fileObjectId)}>下载报备文件:{source.exportFile.fileName}</a>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="surface" style={{ padding: 16 }}>
|
||||
<h3>状态记录</h3>
|
||||
<div className="page-stack" style={{ marginTop: 12 }}>
|
||||
{(task.records ?? []).length ? (
|
||||
task.records!.map((record) => (
|
||||
<div className="detail-grid" key={record.id}>
|
||||
<div>
|
||||
<span>时间</span>
|
||||
<strong>{formatDateTime(record.createdAt)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>动作</span>
|
||||
<strong>{actionLabels[record.action] ?? record.action}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>状态变化</span>
|
||||
<strong>
|
||||
{statusMeta[record.statusBefore ?? '']?.label ?? record.statusBefore ?? '-'} → {statusMeta[record.statusAfter]?.label ?? record.statusAfter}
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>说明</span>
|
||||
<strong>{record.reason || '-'}</strong>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="muted">暂无状态记录</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>;
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export function AdminReportTasksPage() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const [tasks, setTasks] = useState<ReportTask[]>([]);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [dateRange, setDateRange] = useState<DateRangeValue>({});
|
||||
const [reportType, setReportType] = useState('all');
|
||||
const [status, setStatus] = useState(searchParams.get('scope') === 'pending' ? 'pending' : 'all');
|
||||
const [carrier, setCarrier] = useState('all');
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||
const [material, setMaterial] = useState<SingleReportMaterialDetail | null>(null);
|
||||
const [detailTask, setDetailTask] = useState<ReportTask | null>(null);
|
||||
const [statusTask, setStatusTask] = useState<ReportTask | null>(null);
|
||||
const [nextStatus, setNextStatus] = useState('approved');
|
||||
@@ -81,14 +159,18 @@ export function AdminReportTasksPage() {
|
||||
const pageSize = 10;
|
||||
|
||||
function loadData(targetPage = page) {
|
||||
adminApi.listReportTasksPage({
|
||||
reportType: reportType === 'all' ? undefined : reportType as 'signature' | 'drainage',
|
||||
keyword: keyword || undefined,
|
||||
createdAtFrom: dateRange.start || undefined,
|
||||
createdAtTo: dateRange.end || undefined,
|
||||
page: targetPage,
|
||||
pageSize,
|
||||
})
|
||||
adminApi
|
||||
.listReportDetailsPage({
|
||||
signatureId: searchParams.get('signatureId') || undefined,
|
||||
reportType: reportType === 'all' ? undefined : (reportType as 'signature' | 'drainage'),
|
||||
status: status === 'all' ? undefined : status,
|
||||
carrier: carrier === 'all' ? undefined : carrier,
|
||||
keyword: keyword || undefined,
|
||||
createdAtFrom: dateRange.start || undefined,
|
||||
createdAtTo: dateRange.end || undefined,
|
||||
page: targetPage,
|
||||
pageSize,
|
||||
})
|
||||
.then((result) => {
|
||||
setTasks(result.items);
|
||||
setTotal(result.total);
|
||||
@@ -103,22 +185,24 @@ export function AdminReportTasksPage() {
|
||||
|
||||
async function saveTaskStatus() {
|
||||
if (!statusTask) return;
|
||||
const chosen = selected.size ? tasks.filter((task) => selected.has(task.id)) : [statusTask];
|
||||
setBusy(true);
|
||||
try {
|
||||
await adminApi.changeReportTaskStatuses({
|
||||
items: [{
|
||||
signatureId: statusTask.signatureId,
|
||||
channelId: statusTask.channelId,
|
||||
carrier: statusTask.carrier ?? undefined,
|
||||
reportType: statusTask.reportType,
|
||||
drainageItemId: statusTask.drainageItemId ?? undefined,
|
||||
items: chosen.map((task) => ({
|
||||
signatureId: task.signatureId,
|
||||
channelId: task.channelId,
|
||||
carrier: task.carrier ?? undefined,
|
||||
reportType: task.reportType,
|
||||
drainageItemId: task.drainageItemId ?? undefined,
|
||||
status: nextStatus,
|
||||
}],
|
||||
})),
|
||||
reason: statusReason.trim() || undefined,
|
||||
sourceEntry: 'report_task',
|
||||
});
|
||||
setStatusTask(null);
|
||||
setStatusReason('');
|
||||
setSelected(new Set());
|
||||
loadData();
|
||||
} catch (failure) {
|
||||
setError(failure instanceof Error ? failure.message : '报备状态保存失败');
|
||||
@@ -127,56 +211,307 @@ export function AdminReportTasksPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function openMaterial(task: ReportTask) {
|
||||
try {
|
||||
setMaterial(
|
||||
await adminApi.getSingleReportMaterialDetail({
|
||||
reportType: task.reportType,
|
||||
signatureId: task.signatureId,
|
||||
channelId: task.channelId,
|
||||
carrier: task.carrier ?? undefined,
|
||||
drainageItemId: task.drainageItemId ?? undefined,
|
||||
batchItemId: task.exportItems?.[0]?.batchItem.id,
|
||||
}),
|
||||
);
|
||||
} catch (failure) {
|
||||
setError(failure instanceof Error ? failure.message : '报备资料加载失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function exportMaterial(task: ReportTask) {
|
||||
try {
|
||||
const blob = await adminApi.exportSingleReportMaterial({
|
||||
reportType: task.reportType,
|
||||
signatureId: task.signatureId,
|
||||
channelId: task.channelId,
|
||||
carrier: task.carrier ?? undefined,
|
||||
drainageItemId: task.drainageItemId ?? undefined,
|
||||
batchItemId: task.exportItems?.[0]?.batchItem.id,
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.download = `${task.signature?.name ?? '签名'}-${task.channel?.name ?? '通道'}.xlsx`;
|
||||
anchor.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (failure) {
|
||||
setError(failure instanceof Error ? failure.message : '单条资料导出失败');
|
||||
}
|
||||
}
|
||||
|
||||
const columns: Array<TableColumn<ReportTask>> = [
|
||||
{ key: 'target', title: '报备对象', render: (record) => <div><strong>{taskTargetLabel(record)}</strong><div className="muted">{record.reportType === 'drainage' ? '引流信息' : '签名'} · {record.signature?.tenant?.name ?? record.tenantId}</div></div> },
|
||||
{
|
||||
key: 'select',
|
||||
title: '',
|
||||
width: '44px',
|
||||
render: (record) => (
|
||||
<input
|
||||
aria-label={`选择${taskTargetLabel(record)}`}
|
||||
checked={selected.has(record.id)}
|
||||
onChange={() =>
|
||||
setSelected((current) => {
|
||||
const next = new Set(current);
|
||||
if (next.has(record.id)) next.delete(record.id);
|
||||
else next.add(record.id);
|
||||
return next;
|
||||
})
|
||||
}
|
||||
type="checkbox"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'target',
|
||||
title: '报备对象',
|
||||
render: (record) => (
|
||||
<div>
|
||||
<strong>{taskTargetLabel(record)}</strong>
|
||||
<div className="muted">
|
||||
{record.reportType === 'drainage' ? '引流信息' : '签名'} · {record.signature?.tenant?.name ?? record.tenantId}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{ key: 'application', title: '企业应用', render: (record) => record.signature?.application?.name ?? '未指定应用' },
|
||||
{ key: 'channel', title: '通道/运营商', render: (record) => <div><strong>{record.channel?.name ?? record.channelId}</strong>{record.reportType !== 'drainage' ? <div className="muted">{record.carrier ? <CarrierTag carrier={record.carrier} /> : '历史通道级(未拆分)'}</div> : null}</div> },
|
||||
{ key: 'batch', title: '批次/版本', render: (record) => {
|
||||
const source = record.exportItems?.[0];
|
||||
return source ? <div><strong>{source.batchItem.batch.batchNo}</strong><div className="muted">V{source.batchItem.materialVersion} · 第{source.rowNumber}行</div></div> : '-';
|
||||
} },
|
||||
{ key: 'status', title: '状态', render: (record) => <Tag tone={(statusMeta[record.status] ?? { tone: 'info' as const }).tone}>{(statusMeta[record.status] ?? { label: record.status }).label}</Tag> },
|
||||
{
|
||||
key: 'channel',
|
||||
title: '通道/运营商',
|
||||
render: (record) => (
|
||||
<div>
|
||||
<strong>{record.channel?.name ?? record.channelId}</strong>
|
||||
{record.reportType !== 'drainage' ? <div className="muted">{record.carrier ? <CarrierTag carrier={record.carrier} /> : '历史通道级(未拆分)'}</div> : null}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'batch',
|
||||
title: '批次/版本',
|
||||
render: (record) => {
|
||||
const source = record.exportItems?.[0];
|
||||
return source ? (
|
||||
<div>
|
||||
<strong>{source.batchItem.batch.batchNo}</strong>
|
||||
<div className="muted">
|
||||
V{source.batchItem.materialVersion} · 第{source.rowNumber}行
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
'-'
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
title: '状态',
|
||||
render: (record) => <Tag tone={(statusMeta[record.status] ?? { tone: 'info' as const }).tone}>{(statusMeta[record.status] ?? { label: record.status }).label}</Tag>,
|
||||
},
|
||||
{ key: 'time', title: '更新时间', render: (record) => formatDateTime(record.updatedAt ?? record.createdAt) },
|
||||
{ key: 'actions', title: '操作', align: 'right', render: (record) => <div className="table-actions"><Button icon={<Eye size={14} />} onClick={() => setDetailTask(record)} size="sm" variant="ghost">详情</Button><Button onClick={() => {
|
||||
setStatusTask(record);
|
||||
setNextStatus(record.status);
|
||||
setStatusReason('');
|
||||
}} size="sm" variant="ghost">修改状态</Button></div> },
|
||||
{
|
||||
key: 'actions',
|
||||
title: '操作',
|
||||
align: 'right',
|
||||
render: (record) => (
|
||||
<div className="table-actions">
|
||||
<Button icon={<Eye size={14} />} onClick={() => void openMaterial(record)} size="sm" variant="ghost">
|
||||
查看报备资料
|
||||
</Button>
|
||||
{record.reportType !== 'drainage' ? (
|
||||
<Button icon={<Download size={14} />} onClick={() => void exportMaterial(record)} size="sm" variant="ghost">
|
||||
导出
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
onClick={() => {
|
||||
setSelected(new Set());
|
||||
setStatusTask(record);
|
||||
setNextStatus(record.status);
|
||||
setStatusReason('');
|
||||
}}
|
||||
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><p>签名明细对应一个签名在具体通道和运营商下的当前状态;引流信息继续按具体通道展示。</p></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} />
|
||||
<DateRangeInput label="创建时间" onChange={setDateRange} value={dateRange} />
|
||||
<div className="admin-task-filter__actions"><Button icon={<Search size={16} />} onClick={() => { if (page !== 1) setPage(1); else loadData(1); }}>查询</Button><Button onClick={() => {
|
||||
setKeyword('');
|
||||
setDateRange({});
|
||||
setReportType('all');
|
||||
}} variant="ghost">重置</Button></div>
|
||||
</div>
|
||||
<div className="surface report-task-table-card"><Table columns={columns} data={tasks} 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))} />
|
||||
{detailTask ? <TaskDetailModal onClose={() => setDetailTask(null)} task={detailTask} /> : null}
|
||||
<Modal footer={<><Button disabled={busy} onClick={() => setStatusTask(null)} variant="ghost">取消</Button><Button disabled={busy} onClick={() => void saveTaskStatus()}>{busy ? '保存中…' : '保存'}</Button></>} onClose={() => setStatusTask(null)} open={Boolean(statusTask)} title="修改报备状态">
|
||||
{statusTask ? <div className="page-stack">
|
||||
<div className="detail-grid">
|
||||
<div><span>报备对象</span><strong>{taskTargetLabel(statusTask)}</strong></div>
|
||||
<div><span>通道</span><strong>{statusTask.channel?.name ?? statusTask.channelId}</strong></div>
|
||||
<div><span>当前状态</span><strong>{statusMeta[statusTask.status]?.label ?? statusTask.status}</strong></div>
|
||||
return (
|
||||
<section className="page-stack admin-sms-task-page report-task-page">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<Breadcrumb items={['报备工作台', '通道报备明细']} />
|
||||
<h1>通道报备明细</h1>
|
||||
<p>按企业应用 × 签名/引流对象 × 通道 × 运营商展示,未生成任务的“未报备”明细也会显示。</p>
|
||||
</div>
|
||||
<Select label="修改为" onChange={(event) => setNextStatus(event.target.value)} options={[
|
||||
{ label: '未报备', value: 'pending' },
|
||||
{ label: '资料待补充', value: 'waiting_material' },
|
||||
{ label: '报备中', value: 'reporting' },
|
||||
{ label: '报备通过', value: 'approved' },
|
||||
{ label: '报备失败', value: 'failed' },
|
||||
{ label: '放弃报备', value: 'abandoned' },
|
||||
]} value={nextStatus} />
|
||||
<Textarea label="修改原因(选填)" onChange={(event) => setStatusReason(event.target.value)} placeholder="可填写供应商反馈或人工处理说明" rows={3} value={statusReason} />
|
||||
</div> : null}
|
||||
</Modal>
|
||||
</section>;
|
||||
<Button
|
||||
disabled={!selected.size}
|
||||
onClick={() => {
|
||||
const first = tasks.find((task) => selected.has(task.id));
|
||||
if (first) {
|
||||
setStatusTask(first);
|
||||
setNextStatus('reporting');
|
||||
}
|
||||
}}
|
||||
>
|
||||
批量修改状态({selected.size})
|
||||
</Button>
|
||||
</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}
|
||||
/>
|
||||
<Select
|
||||
label="运营商"
|
||||
onChange={(event) => setCarrier(event.target.value)}
|
||||
options={[
|
||||
{ label: '全部运营商', value: 'all' },
|
||||
{ label: '移动', value: 'mobile' },
|
||||
{ label: '联通', value: 'unicom' },
|
||||
{ label: '电信', value: 'telecom' },
|
||||
]}
|
||||
value={carrier}
|
||||
/>
|
||||
<Select label="报备状态" onChange={(event) => setStatus(event.target.value)} options={[{ label: '全部状态', value: 'all' }, ...Object.entries(statusMeta).map(([value, meta]) => ({ label: meta.label, value }))]} value={status} />
|
||||
<DateRangeInput label="创建时间" onChange={setDateRange} value={dateRange} />
|
||||
<div className="admin-task-filter__actions">
|
||||
<Button
|
||||
icon={<Search size={16} />}
|
||||
onClick={() => {
|
||||
if (page !== 1) setPage(1);
|
||||
else loadData(1);
|
||||
}}
|
||||
>
|
||||
查询
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setKeyword('');
|
||||
setDateRange({});
|
||||
setReportType('all');
|
||||
setCarrier('all');
|
||||
setStatus('all');
|
||||
}}
|
||||
variant="ghost"
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="surface report-task-table-card">
|
||||
<Table columns={columns} data={tasks} 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))} />
|
||||
{detailTask ? <TaskDetailModal onClose={() => setDetailTask(null)} task={detailTask} /> : null}
|
||||
{material ? (
|
||||
<Modal footer={<Button onClick={() => setMaterial(null)}>关闭</Button>} onClose={() => setMaterial(null)} open size="xl" title="查看报备资料">
|
||||
<div className="page-stack">
|
||||
<div className="detail-grid">
|
||||
<div>
|
||||
<span>签名</span>
|
||||
<strong>{material.signatureName}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>企业/应用</span>
|
||||
<strong>
|
||||
{material.tenant.name} · {material.application?.name ?? '-'}
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>通道/版本</span>
|
||||
<strong>
|
||||
{material.channel.name} · V{material.materialVersion}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div className="report-material-detail-list">
|
||||
{material.fields.map((field) => (
|
||||
<div className={field.missing ? 'is-missing' : ''} key={field.id}>
|
||||
<span>
|
||||
{field.exportName || field.name}
|
||||
{field.required ? ' *' : ''}
|
||||
</span>
|
||||
<strong>{typeof field.value === 'object' ? String((field.value as Record<string, unknown>)?.fileName ?? '-') : String(field.value ?? '-')}</strong>
|
||||
</div>
|
||||
))}
|
||||
{material.historicalFields.map((field) => (
|
||||
<div key={field.code}>
|
||||
<span>{field.name}(历史字段)</span>
|
||||
<strong>{String(field.value ?? '-')}</strong>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
) : null}
|
||||
<Modal
|
||||
footer={
|
||||
<>
|
||||
<Button disabled={busy} onClick={() => setStatusTask(null)} variant="ghost">
|
||||
取消
|
||||
</Button>
|
||||
<Button disabled={busy} onClick={() => void saveTaskStatus()}>
|
||||
{busy ? '保存中…' : '保存'}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
onClose={() => setStatusTask(null)}
|
||||
open={Boolean(statusTask)}
|
||||
title="修改报备状态"
|
||||
>
|
||||
{statusTask ? (
|
||||
<div className="page-stack">
|
||||
<div className="detail-grid">
|
||||
<div>
|
||||
<span>报备对象</span>
|
||||
<strong>{selected.size ? `已选择 ${selected.size} 条明细` : taskTargetLabel(statusTask)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>通道</span>
|
||||
<strong>{statusTask.channel?.name ?? statusTask.channelId}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>当前状态</span>
|
||||
<strong>{statusMeta[statusTask.status]?.label ?? statusTask.status}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<Select
|
||||
label="修改为"
|
||||
onChange={(event) => setNextStatus(event.target.value)}
|
||||
options={[
|
||||
{ label: '未报备', value: 'pending' },
|
||||
{ label: '资料待补充', value: 'waiting_material' },
|
||||
{ label: '报备中', value: 'reporting' },
|
||||
{ label: '报备通过', value: 'approved' },
|
||||
{ label: '报备失败', value: 'failed' },
|
||||
{ label: '放弃报备', value: 'abandoned' },
|
||||
]}
|
||||
value={nextStatus}
|
||||
/>
|
||||
<Textarea label="修改原因(选填)" onChange={(event) => setStatusReason(event.target.value)} placeholder="可填写供应商反馈或人工处理说明" rows={3} value={statusReason} />
|
||||
</div>
|
||||
) : null}
|
||||
</Modal>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user