656 lines
23 KiB
TypeScript
656 lines
23 KiB
TypeScript
import { useEffect, useRef, useState } from 'react';
|
||
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';
|
||
import { ReportExportFormatModal, type ReportWorkbookFormat } from './ReportExportFormatModal';
|
||
|
||
function materialValue(value: unknown) {
|
||
const file = value && typeof value === 'object' ? (value as Record<string, unknown>) : {};
|
||
const fileObjectId = String(file.fileObjectId ?? '');
|
||
const fileName = String(file.fileName ?? fileObjectId ?? '-');
|
||
if (
|
||
fileObjectId &&
|
||
(String(file.contentType ?? '').startsWith('image/') || /\.(?:png|jpe?g|gif|webp|bmp)$/i.test(fileName))
|
||
)
|
||
return (
|
||
<div className="report-material-image-value">
|
||
<img alt={fileName} src={fileDownloadUrl(fileObjectId, 'inline')} />
|
||
<a href={fileDownloadUrl(fileObjectId)}>{fileName}</a>
|
||
</div>
|
||
);
|
||
if (fileObjectId) return <a href={fileDownloadUrl(fileObjectId)}>{fileName}</a>;
|
||
return String(value ?? '-');
|
||
}
|
||
|
||
const statusMeta: Record<string, { label: string; tone: 'neutral' | 'info' | 'success' | 'warning' | 'danger' }> = {
|
||
pending: { label: '未报备', tone: 'neutral' },
|
||
waiting_material: { label: '资料待补充', tone: 'warning' },
|
||
reporting: { label: '报备中', tone: 'warning' },
|
||
exporting: { label: '报备中', tone: 'warning' },
|
||
approved: { label: '报备通过', tone: 'success' },
|
||
failed: { label: '报备失败', tone: 'danger' },
|
||
rejected: { label: '报备失败', tone: 'danger' },
|
||
abandoned: { label: '已放弃', tone: 'neutral' },
|
||
};
|
||
|
||
const actionLabels: Record<string, string> = {
|
||
create: '创建报备明细',
|
||
batch_export: '生成报备批次',
|
||
export: '生成报备文件',
|
||
manual_status_change: '人工修改状态',
|
||
receipt_import: '历史回执导入',
|
||
};
|
||
|
||
function taskTargetLabel(task: ReportTask) {
|
||
if (task.reportType !== 'drainage') return task.signature?.name ?? task.signatureId;
|
||
return task.drainageInfo?.url || task.drainageItemId || '引流信息';
|
||
}
|
||
|
||
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>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Modal>
|
||
);
|
||
}
|
||
|
||
export function AdminReportTasksPage() {
|
||
const [searchParams] = useSearchParams();
|
||
const initialStatus = searchParams.get('scope') === 'pending' ? 'pending' : 'all';
|
||
const [tasks, setTasks] = useState<ReportTask[]>([]);
|
||
const [keyword, setKeyword] = useState('');
|
||
const [dateRange, setDateRange] = useState<DateRangeValue>({});
|
||
const [reportType, setReportType] = useState('all');
|
||
const [status, setStatus] = useState(initialStatus);
|
||
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');
|
||
const [statusReason, setStatusReason] = useState('');
|
||
const [error, setError] = useState('');
|
||
const [busy, setBusy] = useState(false);
|
||
const [exportTask, setExportTask] = useState<ReportTask | null>(null);
|
||
const [exportBusy, setExportBusy] = useState(false);
|
||
const [page, setPage] = useState(1);
|
||
const [pageSize, setPageSize] = useState(25);
|
||
const requestId = useRef(0);
|
||
const [total, setTotal] = useState(0);
|
||
const [appliedFilters, setAppliedFilters] = useState({
|
||
keyword: '',
|
||
dateRange: {} as DateRangeValue,
|
||
reportType: 'all',
|
||
status: initialStatus,
|
||
carrier: 'all',
|
||
});
|
||
function loadData(targetPage = page, filters = appliedFilters) {
|
||
const id = ++requestId.current;
|
||
adminApi
|
||
.listReportDetailsPage({
|
||
signatureId: searchParams.get('signatureId') || undefined,
|
||
reportType: filters.reportType === 'all' ? undefined : (filters.reportType as 'signature' | 'drainage'),
|
||
status: filters.status === 'all' ? undefined : filters.status,
|
||
carrier: filters.carrier === 'all' ? undefined : filters.carrier,
|
||
keyword: filters.keyword || undefined,
|
||
createdAtFrom: filters.dateRange.start || undefined,
|
||
createdAtTo: filters.dateRange.end || undefined,
|
||
page: targetPage,
|
||
pageSize,
|
||
})
|
||
.then((result) => {
|
||
if (id !== requestId.current) return;
|
||
setTasks(result.items);
|
||
setTotal(result.total);
|
||
setSelected(new Set());
|
||
setError('');
|
||
})
|
||
.catch((failure: Error) => {
|
||
if (id === requestId.current) setError(failure.message || '报备明细加载失败');
|
||
});
|
||
}
|
||
|
||
useEffect(() => {
|
||
loadData(page);
|
||
return () => {
|
||
requestId.current += 1;
|
||
};
|
||
}, [page, pageSize]);
|
||
|
||
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: 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 : '报备状态保存失败');
|
||
} finally {
|
||
setBusy(false);
|
||
}
|
||
}
|
||
|
||
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, outputFormat: ReportWorkbookFormat) {
|
||
try {
|
||
setExportBusy(true);
|
||
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,
|
||
outputFormat,
|
||
});
|
||
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);
|
||
setExportTask(null);
|
||
} catch (failure) {
|
||
setError(failure instanceof Error ? failure.message : '单条资料导出失败');
|
||
} finally {
|
||
setExportBusy(false);
|
||
}
|
||
}
|
||
|
||
const columns: Array<TableColumn<ReportTask>> = [
|
||
{
|
||
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>
|
||
<div className="muted">
|
||
{record.carrier ? <CarrierTag carrier={record.carrier} /> : '历史通道级(未拆分)'}
|
||
</div>
|
||
</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={() => void openMaterial(record)} size="sm" variant="ghost">
|
||
查看报备资料
|
||
</Button>
|
||
{record.reportType !== 'drainage' ? (
|
||
<Button icon={<Download size={14} />} onClick={() => setExportTask(record)} size="sm" variant="ghost">
|
||
导出
|
||
</Button>
|
||
) : null}
|
||
<Button
|
||
onClick={() => {
|
||
setSelected(new Set());
|
||
setStatusTask(record);
|
||
setNextStatus(record.status);
|
||
setStatusReason('');
|
||
}}
|
||
size="sm"
|
||
variant="ghost"
|
||
>
|
||
修改状态
|
||
</Button>
|
||
</div>
|
||
),
|
||
},
|
||
];
|
||
const allCurrentPageSelected = tasks.length > 0 && tasks.every((task) => selected.has(task.id));
|
||
|
||
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 className="page-heading__actions">
|
||
<Button
|
||
disabled={!tasks.length}
|
||
onClick={() => setSelected(allCurrentPageSelected ? new Set() : new Set(tasks.map((task) => task.id)))}
|
||
variant="ghost"
|
||
>
|
||
{allCurrentPageSelected ? '取消全选' : '全选当页'}
|
||
</Button>
|
||
<Button
|
||
disabled={!selected.size}
|
||
onClick={() => {
|
||
const first = tasks.find((task) => selected.has(task.id));
|
||
if (first) {
|
||
setStatusTask(first);
|
||
setNextStatus('reporting');
|
||
}
|
||
}}
|
||
>
|
||
批量修改状态({selected.size})
|
||
</Button>
|
||
</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}
|
||
/>
|
||
<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)
|
||
.filter(([value]) => !['exporting', 'rejected'].includes(value))
|
||
.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={() => {
|
||
const filters = { keyword: keyword.trim(), dateRange, reportType, status, carrier };
|
||
setAppliedFilters(filters);
|
||
if (page !== 1) setPage(1);
|
||
else loadData(1, filters);
|
||
}}
|
||
>
|
||
查询
|
||
</Button>
|
||
<Button
|
||
onClick={() => {
|
||
setKeyword('');
|
||
setDateRange({});
|
||
setReportType('all');
|
||
setCarrier('all');
|
||
setStatus('all');
|
||
const filters = {
|
||
keyword: '',
|
||
dateRange: {} as DateRangeValue,
|
||
reportType: 'all',
|
||
status: 'all',
|
||
carrier: 'all',
|
||
};
|
||
setAppliedFilters(filters);
|
||
if (page !== 1) setPage(1);
|
||
else loadData(1, filters);
|
||
}}
|
||
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}
|
||
onPageSizeChange={(size) => {
|
||
setPageSize(size);
|
||
setPage(1);
|
||
setSelected(new Set());
|
||
}}
|
||
onPrevious={() => setPage((current) => Math.max(1, current - 1))}
|
||
page={page}
|
||
pageSize={pageSize}
|
||
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} · {material.channel.code} · 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.name}({field.code})
|
||
{field.exportName && field.exportName !== field.name ? ` · 导出为“${field.exportName}”` : ''}
|
||
{field.required ? ' *' : ''}
|
||
</span>
|
||
<strong>{materialValue(field.value)}</strong>
|
||
</div>
|
||
))}
|
||
{material.historicalFields.map((field) => (
|
||
<div key={field.code}>
|
||
<span>
|
||
{field.name}({field.code},历史字段)
|
||
</span>
|
||
<strong>{materialValue(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>
|
||
{exportTask ? (
|
||
<ReportExportFormatModal
|
||
busy={exportBusy}
|
||
onClose={() => setExportTask(null)}
|
||
onConfirm={(format) => void exportMaterial(exportTask, format)}
|
||
/>
|
||
) : null}
|
||
</section>
|
||
);
|
||
}
|