feat: support WPS report material workbooks
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user