feat: 异步解析大文件WPS报备资料
This commit is contained in:
@@ -6,6 +6,7 @@ import { ReportMaterialImportModal } from './ReportMaterialImportModal';
|
||||
const { adminApi } = vi.hoisted(() => ({
|
||||
adminApi: {
|
||||
analyzeReportMaterialImport: vi.fn(),
|
||||
getReportMaterialImportAnalysisStatus: vi.fn(),
|
||||
listDrainageFields: vi.fn(),
|
||||
listEnterpriseApplicationOptions: vi.fn(),
|
||||
listReportImportProfiles: vi.fn(),
|
||||
@@ -27,6 +28,15 @@ describe('ReportMaterialImportModal mapping profile action', () => {
|
||||
adminApi.listReportImportProfiles.mockResolvedValue([]);
|
||||
adminApi.analyzeReportMaterialImport.mockResolvedValue({
|
||||
id: 'analysis-1',
|
||||
status: 'queued',
|
||||
progress: 10,
|
||||
progressStage: '文件已上传,等待解析',
|
||||
});
|
||||
adminApi.getReportMaterialImportAnalysisStatus.mockResolvedValue({
|
||||
id: 'analysis-1',
|
||||
status: 'analyzed',
|
||||
progress: 100,
|
||||
progressStage: '解析完成',
|
||||
columns: [
|
||||
{ sourceColumnIndex: 0, columnLetter: 'A', sourceHeader: '签名', sourceHeaderPath: '签名', imageCount: 0 },
|
||||
],
|
||||
@@ -59,5 +69,5 @@ describe('ReportMaterialImportModal mapping profile action', () => {
|
||||
expect(screen.getByRole('button', { name: '本次将保存/更新映射方案' })).toHaveAttribute('aria-pressed', 'true'),
|
||||
);
|
||||
expect(screen.getByLabelText('映射方案名称')).toBeVisible();
|
||||
});
|
||||
}, 10_000);
|
||||
});
|
||||
|
||||
@@ -25,6 +25,7 @@ type AnalyzeResult = {
|
||||
rows: Array<{ rowNumber: number; values: Record<string, string>; imageColumns: number[] }>;
|
||||
suggestedMappings: ReportImportMapping[];
|
||||
};
|
||||
type AnalysisProgress = { id: string; status: string; progress: number; progressStage?: string; errorMessage?: string };
|
||||
|
||||
const transforms = [
|
||||
{ label: '保持原值', value: '' },
|
||||
@@ -60,6 +61,7 @@ export function ReportMaterialImportModal({ onClose, onCompleted }: { onClose: (
|
||||
const [headerRowCount, setHeaderRowCount] = useState(1);
|
||||
const [dataStartRow, setDataStartRow] = useState(2);
|
||||
const [analysis, setAnalysis] = useState<AnalyzeResult>();
|
||||
const [analysisJob, setAnalysisJob] = useState<AnalysisProgress>();
|
||||
const [mappings, setMappings] = useState<ReportImportMapping[]>([]);
|
||||
const [profileName, setProfileName] = useState('');
|
||||
const [saveProfile, setSaveProfile] = useState(false);
|
||||
@@ -87,10 +89,52 @@ export function ReportMaterialImportModal({ onClose, onCompleted }: { onClose: (
|
||||
.catch(() => setProfiles([]));
|
||||
}, [reportType]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!analysisJob?.id || !tenantId || analysisJob.status === 'analyzed' || analysisJob.status === 'failed') return;
|
||||
const controller = new AbortController();
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
const poll = async () => {
|
||||
try {
|
||||
const result = await adminApi.getReportMaterialImportAnalysisStatus(
|
||||
analysisJob.id,
|
||||
tenantId,
|
||||
controller.signal,
|
||||
);
|
||||
if (controller.signal.aborted) return;
|
||||
setError('');
|
||||
setAnalysisJob(result);
|
||||
if (result.status === 'analyzed') {
|
||||
const completed = result as AnalyzeResult & AnalysisProgress;
|
||||
setAnalysis(completed);
|
||||
setMappings(completed.suggestedMappings ?? []);
|
||||
const selectedProfile = profiles.find((item) => item.id === profileId);
|
||||
if (selectedProfile) setProfileName(selectedProfile.name);
|
||||
return;
|
||||
}
|
||||
if (result.status === 'failed') {
|
||||
setError(result.errorMessage || '文件解析失败');
|
||||
return;
|
||||
}
|
||||
timer = setTimeout(() => void poll(), 1200);
|
||||
} catch (failure) {
|
||||
if (!controller.signal.aborted) {
|
||||
setError(failure instanceof Error ? `${failure.message},正在重试` : '解析进度查询失败,正在重试');
|
||||
timer = setTimeout(() => void poll(), 2000);
|
||||
}
|
||||
}
|
||||
};
|
||||
void poll();
|
||||
return () => {
|
||||
controller.abort();
|
||||
if (timer) clearTimeout(timer);
|
||||
};
|
||||
}, [analysisJob?.id, analysisJob?.status, profileId, profiles, tenantId]);
|
||||
|
||||
function changeReportType(next: ReportType) {
|
||||
setReportType(next);
|
||||
setProfileId('');
|
||||
setAnalysis(undefined);
|
||||
setAnalysisJob(undefined);
|
||||
setMappings([]);
|
||||
}
|
||||
|
||||
@@ -108,18 +152,15 @@ export function ReportMaterialImportModal({ onClose, onCompleted }: { onClose: (
|
||||
setBusy(true);
|
||||
setError('');
|
||||
try {
|
||||
const result = (await adminApi.analyzeReportMaterialImport(file, {
|
||||
const result = await adminApi.analyzeReportMaterialImport(file, {
|
||||
tenantId,
|
||||
applicationId,
|
||||
reportType,
|
||||
headerRowCount,
|
||||
dataStartRow,
|
||||
profileId: profileId || undefined,
|
||||
})) as AnalyzeResult;
|
||||
setAnalysis(result);
|
||||
setMappings(result.suggestedMappings ?? []);
|
||||
const selectedProfile = profiles.find((item) => item.id === profileId);
|
||||
if (selectedProfile) setProfileName(selectedProfile.name);
|
||||
});
|
||||
setAnalysisJob(result);
|
||||
} catch (failure) {
|
||||
setError(failure instanceof Error ? failure.message : '文件解析失败');
|
||||
} finally {
|
||||
@@ -216,8 +257,21 @@ export function ReportMaterialImportModal({ onClose, onCompleted }: { onClose: (
|
||||
{busy ? '提交中...' : '提交导入审核'}
|
||||
</Button>
|
||||
) : (
|
||||
<Button disabled={busy || !file || !tenantId || !applicationId} onClick={() => void analyze()}>
|
||||
{busy ? '解析中...' : '解析文件并配置映射'}
|
||||
<Button
|
||||
disabled={
|
||||
busy ||
|
||||
Boolean(analysisJob && !['failed', 'analyzed'].includes(analysisJob.status)) ||
|
||||
!file ||
|
||||
!tenantId ||
|
||||
!applicationId
|
||||
}
|
||||
onClick={() => void analyze()}
|
||||
>
|
||||
{busy
|
||||
? '上传中...'
|
||||
: analysisJob && !['failed', 'analyzed'].includes(analysisJob.status)
|
||||
? '后台解析中...'
|
||||
: '解析文件并配置映射'}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
@@ -306,10 +360,33 @@ export function ReportMaterialImportModal({ onClose, onCompleted }: { onClose: (
|
||||
onChange={(event) => {
|
||||
setFile(event.target.files?.[0]);
|
||||
setAnalysis(undefined);
|
||||
setAnalysisJob(undefined);
|
||||
setError('');
|
||||
}}
|
||||
type="file"
|
||||
/>
|
||||
</label>
|
||||
{analysisJob && analysisJob.status !== 'analyzed' ? (
|
||||
<div className="report-import-analysis-progress" aria-live="polite">
|
||||
<div>
|
||||
<strong>{analysisJob.progressStage || '正在处理'}</strong>
|
||||
<span>{Math.max(0, Math.min(100, analysisJob.progress))}%</span>
|
||||
</div>
|
||||
<div
|
||||
className="batch-progress__track"
|
||||
role="progressbar"
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
aria-valuenow={analysisJob.progress}
|
||||
>
|
||||
<span
|
||||
className="batch-progress__bar batch-progress__bar--sending"
|
||||
style={{ width: `${Math.max(0, Math.min(100, analysisJob.progress))}%` }}
|
||||
/>
|
||||
</div>
|
||||
<small>文件上传完成后可由后台持续解析,页面通过真实任务状态更新进度。</small>
|
||||
</div>
|
||||
) : null}
|
||||
{analysis ? (
|
||||
<div className="report-import-mapping">
|
||||
<div className="channel-field-section-head">
|
||||
|
||||
Reference in New Issue
Block a user