feat: support WPS report material workbooks

This commit is contained in:
hectorzhao
2026-09-04 17:10:04 +08:00
parent 48d0363920
commit fb39c8b606
29 changed files with 2737 additions and 734 deletions
+127 -23
View File
@@ -2,8 +2,41 @@ import { useEffect, 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 {
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' },
@@ -119,7 +152,8 @@ function TaskDetailModal({ task, onClose }: { task: ReportTask; onClose: () => v
<div>
<span></span>
<strong>
{statusMeta[record.statusBefore ?? '']?.label ?? record.statusBefore ?? '-'} {statusMeta[record.statusAfter]?.label ?? record.statusAfter}
{statusMeta[record.statusBefore ?? '']?.label ?? record.statusBefore ?? '-'} {' '}
{statusMeta[record.statusAfter]?.label ?? record.statusAfter}
</strong>
</div>
<div>
@@ -155,9 +189,17 @@ export function AdminReportTasksPage() {
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 [total, setTotal] = useState(0);
const [appliedFilters, setAppliedFilters] = useState({ keyword: '', dateRange: {} as DateRangeValue, reportType: 'all', status: initialStatus, carrier: 'all' });
const [appliedFilters, setAppliedFilters] = useState({
keyword: '',
dateRange: {} as DateRangeValue,
reportType: 'all',
status: initialStatus,
carrier: 'all',
});
const pageSize = 10;
function loadData(targetPage = page, filters = appliedFilters) {
@@ -231,8 +273,9 @@ export function AdminReportTasksPage() {
}
}
async function exportMaterial(task: ReportTask) {
async function exportMaterial(task: ReportTask, outputFormat: ReportWorkbookFormat) {
try {
setExportBusy(true);
const blob = await adminApi.exportSingleReportMaterial({
reportType: task.reportType,
signatureId: task.signatureId,
@@ -240,6 +283,7 @@ export function AdminReportTasksPage() {
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');
@@ -247,8 +291,11 @@ export function AdminReportTasksPage() {
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);
}
}
@@ -280,7 +327,8 @@ export function AdminReportTasksPage() {
<div>
<strong>{taskTargetLabel(record)}</strong>
<div className="muted">
{record.reportType === 'drainage' ? '引流信息' : '签名'} · {record.signature?.tenant?.name ?? record.tenantId}
{record.reportType === 'drainage' ? '引流信息' : '签名'} ·{' '}
{record.signature?.tenant?.name ?? record.tenantId}
</div>
</div>
),
@@ -292,7 +340,11 @@ export function AdminReportTasksPage() {
render: (record) => (
<div>
<strong>{record.channel?.name ?? record.channelId}</strong>
{record.reportType !== 'drainage' ? <div className="muted">{record.carrier ? <CarrierTag carrier={record.carrier} /> : '历史通道级(未拆分)'}</div> : null}
{record.reportType !== 'drainage' ? (
<div className="muted">
{record.carrier ? <CarrierTag carrier={record.carrier} /> : '历史通道级(未拆分)'}
</div>
) : null}
</div>
),
},
@@ -316,7 +368,11 @@ export function AdminReportTasksPage() {
{
key: 'status',
title: '状态',
render: (record) => <Tag tone={(statusMeta[record.status] ?? { tone: 'info' as const }).tone}>{(statusMeta[record.status] ?? { label: record.status }).label}</Tag>,
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) },
{
@@ -329,7 +385,7 @@ export function AdminReportTasksPage() {
</Button>
{record.reportType !== 'drainage' ? (
<Button icon={<Download size={14} />} onClick={() => void exportMaterial(record)} size="sm" variant="ghost">
<Button icon={<Download size={14} />} onClick={() => setExportTask(record)} size="sm" variant="ghost">
</Button>
) : null}
@@ -362,9 +418,7 @@ export function AdminReportTasksPage() {
<div className="page-heading__actions">
<Button
disabled={!tasks.length}
onClick={() =>
setSelected(allCurrentPageSelected ? new Set() : new Set(tasks.map((task) => task.id)))
}
onClick={() => setSelected(allCurrentPageSelected ? new Set() : new Set(tasks.map((task) => task.id)))}
variant="ghost"
>
{allCurrentPageSelected ? '取消全选' : '全选当页'}
@@ -385,7 +439,12 @@ export function AdminReportTasksPage() {
</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} />
<Input
label="企业/应用/通道/报备对象"
onChange={(event) => setKeyword(event.target.value)}
placeholder="搜索报备明细"
value={keyword}
/>
<Select
label="报备类型"
onChange={(event) => setReportType(event.target.value)}
@@ -407,7 +466,15 @@ export function AdminReportTasksPage() {
]}
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} />
<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
@@ -428,7 +495,13 @@ export function AdminReportTasksPage() {
setReportType('all');
setCarrier('all');
setStatus('all');
const filters = { keyword: '', dateRange: {} as DateRangeValue, reportType: 'all', status: 'all', carrier: 'all' };
const filters = {
keyword: '',
dateRange: {} as DateRangeValue,
reportType: 'all',
status: 'all',
carrier: 'all',
};
setAppliedFilters(filters);
if (page !== 1) setPage(1);
else loadData(1, filters);
@@ -442,10 +515,25 @@ export function AdminReportTasksPage() {
<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))} />
<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="查看报备资料">
<Modal
footer={<Button onClick={() => setMaterial(null)}></Button>}
onClose={() => setMaterial(null)}
open
size="xl"
title="查看报备资料"
>
<div className="page-stack">
<div className="detail-grid">
<div>
@@ -459,9 +547,9 @@ export function AdminReportTasksPage() {
</strong>
</div>
<div>
<span>/</span>
<span> / / </span>
<strong>
{material.channel.name} · V{material.materialVersion}
{material.channel.name} · {material.channel.code} · V{material.materialVersion}
</strong>
</div>
</div>
@@ -469,16 +557,19 @@ export function AdminReportTasksPage() {
{material.fields.map((field) => (
<div className={field.missing ? 'is-missing' : ''} key={field.id}>
<span>
{field.exportName || field.name}
{field.name}{field.code}
{field.exportName && field.exportName !== field.name ? ` · 导出为“${field.exportName}` : ''}
{field.required ? ' *' : ''}
</span>
<strong>{typeof field.value === 'object' ? String((field.value as Record<string, unknown>)?.fileName ?? '-') : String(field.value ?? '-')}</strong>
<strong>{materialValue(field.value)}</strong>
</div>
))}
{material.historicalFields.map((field) => (
<div key={field.code}>
<span>{field.name}</span>
<strong>{String(field.value ?? '-')}</strong>
<span>
{field.name}{field.code}
</span>
<strong>{materialValue(field.value)}</strong>
</div>
))}
</div>
@@ -529,10 +620,23 @@ export function AdminReportTasksPage() {
]}
value={nextStatus}
/>
<Textarea label="修改原因(选填)" onChange={(event) => setStatusReason(event.target.value)} placeholder="可填写供应商反馈或人工处理说明" rows={3} value={statusReason} />
<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>
);
}