feat: wire real upload progress and subtype mapping

This commit is contained in:
wren
2026-05-06 18:33:53 +08:00
parent 57c744eb17
commit 61bbf6907b
2 changed files with 265 additions and 186 deletions
+85 -20
View File
@@ -147,8 +147,10 @@ export interface DocumentType {
name: string;
code?: string;
entryModuleId?: number;
entryModuleName?: string | null;
isEnabled?: boolean;
ruleSetIds?: number[];
childDocumentTypeIds?: number[];
}
export interface DocumentSubtypeGroup {
@@ -233,6 +235,12 @@ export interface UploadResult {
error?: string;
}
export interface UploadProgressInfo {
loaded: number;
total: number;
percent: number;
}
// 旧接口上传响应(uploadContractTemplate / appendContractAttachments 仍在使用)
interface LegacyUploadResponse {
success: boolean;
@@ -684,6 +692,7 @@ export async function uploadDocumentToServer(
autoRun: boolean = true,
speed: string = "normal",
jwtToken?: string,
onProgress?: (progress: UploadProgressInfo) => void,
): Promise<{ data: UploadResult } | { error: string; status?: number; payload?: UploadErrorPayload }> {
try {
const formData = new FormData();
@@ -711,7 +720,23 @@ export async function uploadDocumentToServer(
headers["Authorization"] = `Bearer ${jwtToken}`;
}
const response = await axios.post(`${API_BASE_URL}/api/upload`, formData, { headers });
const response = await axios.post(`${API_BASE_URL}/api/upload`, formData, {
headers,
onUploadProgress: (event) => {
const fileBlob = formData.get("file");
const fallbackTotal = fileBlob instanceof Blob ? fileBlob.size : binaryData.byteLength;
const total = Number(event.total || fallbackTotal);
const loaded = Number(event.loaded || 0);
if (!total || !onProgress) {
return;
}
onProgress({
loaded,
total,
percent: Math.min(100, Math.max(0, Number(((loaded / total) * 100).toFixed(2)))),
});
},
});
const body = response.data;
// Result<DocumentUploadVO> envelope
@@ -826,8 +851,6 @@ export async function getDocumentTypes(token?: string): Promise<{data: DocumentT
const params: Record<string, string> = {};
if (selectedModuleId) {
params.entry_module_id = String(selectedModuleId);
} else if (documentTypeIds && documentTypeIds.length > 0) {
params.ids = documentTypeIds.join(",");
}
const headers: Record<string, string> = {};
@@ -835,18 +858,52 @@ export async function getDocumentTypes(token?: string): Promise<{data: DocumentT
headers["Authorization"] = `Bearer ${token}`;
}
const response = await axios.get(`${API_BASE_URL}/api/document-types`, { params, headers });
const [response, groupRoots] = await Promise.all([
axios.get(`${API_BASE_URL}/api/v3/document-type-roots`, { params, headers }),
fetchAllEvaluationPointGroupRoots(token),
]);
const body = response.data;
if (body?.data && Array.isArray(body.data)) {
const types: DocumentType[] = body.data.map((item: { id: number; name: string; code?: string; entryModuleId?: number; isEnabled?: boolean; ruleSetIds?: number[] }) => ({
id: item.id,
name: item.name,
code: item.code,
entryModuleId: item.entryModuleId,
isEnabled: item.isEnabled,
ruleSetIds: item.ruleSetIds,
}));
let types: DocumentType[] = body.data.map((item: {
id: number;
name: string;
code?: string;
entryModuleId?: number | null;
entryModuleName?: string | null;
isEnabled?: boolean;
ruleSetIds?: number[];
}) => {
const matchedRoot = groupRoots.find((root: any) => Number(root?.id || 0) === Number(item.id));
const childDocumentTypeIds = Array.isArray(matchedRoot?.children)
? Array.from(
new Set(
matchedRoot.children
.map((child: any) => Number(child?.document_type_id || 0))
.filter((childId: number) => childId > 0),
),
)
: [];
return {
id: item.id,
name: item.name,
code: item.code,
entryModuleId: item.entryModuleId ?? null,
entryModuleName: item.entryModuleName ?? null,
isEnabled: item.isEnabled,
ruleSetIds: item.ruleSetIds,
childDocumentTypeIds,
};
});
if (!selectedModuleId && documentTypeIds && documentTypeIds.length > 0) {
types = types.filter((item) =>
documentTypeIds.includes(item.id) ||
(item.childDocumentTypeIds || []).some((childId) => documentTypeIds.includes(childId)),
);
}
return { data: types };
}
return { error: body?.message || "获取文档类型失败", status: response.status };
@@ -919,18 +976,26 @@ async function fetchAllEvaluationPointGroupRoots(token?: string): Promise<any[]>
function collectSubtypeGroupsFromRoots(
roots: any[],
documentTypeId: number,
rootOrDocumentTypeId: number,
entryModuleId?: number | null,
): DocumentSubtypeGroup[] {
return dedupeSubtypeGroups(
roots.flatMap((root: any) => {
if (!Array.isArray(root?.children)) return [];
if (entryModuleId && Number(root?.entry_module_id || 0) !== Number(entryModuleId)) {
return [];
}
const scopedRoots = roots.filter((root: any) => {
if (entryModuleId && Number(root?.entry_module_id || 0) !== Number(entryModuleId)) {
return false;
}
return true;
});
const matchedRoot = scopedRoots.find((root: any) => Number(root?.id || 0) === Number(rootOrDocumentTypeId));
if (matchedRoot && Array.isArray(matchedRoot.children)) {
return dedupeSubtypeGroups(matchedRoot.children.map((child: any) => mapSubtypeChild(child, matchedRoot)));
}
return dedupeSubtypeGroups(
scopedRoots.flatMap((root: any) => {
if (!Array.isArray(root?.children)) return [];
return root.children
.filter((child: any) => Number(child?.document_type_id || 0) === Number(documentTypeId))
.filter((child: any) => Number(child?.document_type_id || 0) === Number(rootOrDocumentTypeId))
.map((child: any) => mapSubtypeChild(child, root));
}),
);
+180 -166
View File
@@ -25,6 +25,7 @@ import {
type DocumentType,
type DocumentSubtypeGroup,
type UploadErrorDetails,
type UploadProgressInfo,
type UploadResult,
DocumentStatus
} from "~/api/files/files-upload";
@@ -139,6 +140,7 @@ async function handleFileUpload(
createdBy?: number,
attachments?: File[],
jwtToken?: string,
onProgress?: (progress: UploadProgressInfo) => void,
): Promise<UploadResult> {
const speed = priority === Priority.NORMAL ? "normal" : "urgent";
@@ -154,6 +156,7 @@ async function handleFileUpload(
true,
speed,
jwtToken,
onProgress,
);
if ("error" in response || !response.data) {
@@ -172,6 +175,48 @@ async function handleFileUpload(
return response.data;
}
function mapProcessingStatusToProgress(status: DocumentStatus): number {
switch (status) {
case DocumentStatus.QUEUED:
case DocumentStatus.WAITING:
case DocumentStatus.waiting:
return 15;
case DocumentStatus.CUTTING:
return 35;
case DocumentStatus.EXTRACTIONING:
return 60;
case DocumentStatus.EVALUATIONING:
return 85;
case DocumentStatus.PROCESSED:
return 100;
case DocumentStatus.FAILED:
return 0;
default:
return 15;
}
}
function mapProcessingStatusToSpeed(status: DocumentStatus): string {
switch (status) {
case DocumentStatus.QUEUED:
case DocumentStatus.WAITING:
case DocumentStatus.waiting:
return "排队中";
case DocumentStatus.CUTTING:
return "切分中";
case DocumentStatus.EXTRACTIONING:
return "抽取中";
case DocumentStatus.EVALUATIONING:
return "评查中";
case DocumentStatus.PROCESSED:
return "已完成";
case DocumentStatus.FAILED:
return "处理失败";
default:
return "处理中";
}
}
// 定义action返回数据的类型
type ActionData = {
errors?: {
@@ -199,7 +244,7 @@ export async function action({ request }: ActionFunctionArgs) {
const errors: Record<string, string> = {};
if (!fileType) {
errors.fileType = "上传文件之前请选择文类型";
errors.fileType = "上传文件之前请选择文类型";
}
if (!fileUpload) {
@@ -408,6 +453,9 @@ export default function FilesUpload() {
const hasMultipleSubtypeGroups = subtypeGroups.length > 1;
const singleSubtypeGroup = subtypeGroups.length === 1 ? subtypeGroups[0] : null;
const isSingleDefaultSubtype = !!(singleSubtypeGroup && singleSubtypeGroup.isDefault);
const effectiveSubtypeGroup = selectedSubtypeGroup || singleSubtypeGroup;
const effectiveDocumentTypeId = effectiveSubtypeGroup?.documentTypeId || null;
const effectiveGroupId = effectiveSubtypeGroup?.id || null;
const selectedRootGroupName = selectedSubtypeGroup?.rootGroupName || singleSubtypeGroup?.rootGroupName || "";
const selectedEntryModuleName = selectedSubtypeGroup?.entryModuleName || singleSubtypeGroup?.entryModuleName || "";
@@ -476,7 +524,11 @@ export default function FilesUpload() {
const scopedTypesResponse = await getDocumentTypes(loaderData.frontendJWT || undefined);
if (!cancelled && !scopedTypesResponse.error && scopedTypesResponse.data) {
scopedTypes = scopedTypesResponse.data;
effectiveTypeIds = scopedTypes.map(type => type.id);
effectiveTypeIds = Array.from(
new Set(
scopedTypes.flatMap((type) => type.childDocumentTypeIds || []),
),
);
}
}
@@ -488,13 +540,11 @@ export default function FilesUpload() {
filterDocumentTypes(effectiveTypeIds, scopedTypes, normalizedModuleId);
await filterDocuments(effectiveTypeIds);
if (effectiveTypeIds && effectiveTypeIds.includes(1)) {
setIsContractType(true);
const contractType = scopedTypes.find(type => type.id === 1);
if (contractType) {
setFileType(contractType.id.toString());
setFileTypeError(null);
}
if (scopedTypes.length === 1) {
const onlyType = scopedTypes[0];
setFileType(onlyType.id.toString());
setIsContractType(onlyType.name.includes('合同'));
setFileTypeError(null);
} else {
setIsContractType(false);
}
@@ -527,7 +577,10 @@ export default function FilesUpload() {
}
// 根据 documentTypeIds 过滤文档类型
const filteredTypes = types.filter(type => documentTypeIds.includes(type.id));
const filteredTypes = types.filter(type =>
documentTypeIds.includes(type.id) ||
(type.childDocumentTypeIds || []).some((childId) => documentTypeIds.includes(childId))
);
setDocumentTypesState(filteredTypes);
};
@@ -590,7 +643,6 @@ export default function FilesUpload() {
const [completedFiles, setCompletedFiles] = useState<UploadedFile[]>([]);
// 计时器引用 - 分离为三个独立的定时器
const uploadProgressIntervalRef = useRef<NodeJS.Timeout | null>(null);
const processingStatusIntervalRef = useRef<NodeJS.Timeout | null>(null);
const queueStatusIntervalRef = useRef<NodeJS.Timeout | null>(null); // 原 statusCheckIntervalRef
@@ -828,7 +880,9 @@ export default function FilesUpload() {
setFileTypeError(null);
// 检查是否选择了合同类型
const selectedType = loaderData.documentTypes.find(t => t.id.toString() === value);
const selectedType =
documentTypesState.find(t => t.id.toString() === value) ||
loaderData.documentTypes.find(t => t.id.toString() === value);
const isContract = !!(selectedType && selectedType.name.includes('合同'));
// console.log('【调试-handleFileTypeChange】文件类型检查:', {
// selectedType,
@@ -859,7 +913,7 @@ export default function FilesUpload() {
setFileType("");
setIsContractType(false);
// 如果用户选择了空选项,显示错误信息
setFileTypeError("上传文件之前请选择文类型");
setFileTypeError("上传文件之前请选择文类型");
}
};
@@ -1264,7 +1318,12 @@ export default function FilesUpload() {
const mainFile = mainFiles[0];
// 检查文档名称是否重复
const duplicateResult = await checkDocumentDuplicate(mainFile.name, Number(fileType));
if (!effectiveDocumentTypeId) {
toastService.error('当前子类型缺少可提交的文档类型绑定,请先检查评查点分组配置');
return;
}
const duplicateResult = await checkDocumentDuplicate(mainFile.name, effectiveDocumentTypeId);
if (duplicateResult.is_duplicate) {
const confirmed = window.confirm(
'存在同名文件,会归纳到历史版本中。如需纳入历史版本请点击确定,否则点击取消并重新命名文档后再上传。'
@@ -1291,31 +1350,8 @@ export default function FilesUpload() {
setProcessingSteps(updatedSteps);
}
// 计算总大小并开启与旧逻辑一致的模拟进度(按时间推进到 95%)
const totalSize = filesForProgress.reduce((sum, f) => sum + (f?.size || 0), 0);
const startTime = Date.now();
let lastUpdateTime = startTime;
let lastRatio = 0;
const estimatedUploadTime = Math.max(
(totalSize / (1024 * 1024)) / 3 * 1000, // 3MB/s 估算
1000
);
if (uploadProgressIntervalRef.current) {
clearInterval(uploadProgressIntervalRef.current);
}
uploadProgressIntervalRef.current = setInterval(() => {
const now = Date.now();
const deltaSec = (now - lastUpdateTime) / 1000;
const ratio = Math.min((now - startTime) / estimatedUploadTime, 0.95);
// 计算瞬时速度(基于比例变化)
const deltaRatio = Math.max(ratio - lastRatio, 0);
const bytesPerSec = deltaSec > 0 ? (totalSize * deltaRatio) / deltaSec : 0;
lastRatio = ratio;
lastUpdateTime = now;
setUploadSpeed(`${formatFileSize(bytesPerSec)}/s`);
setUploadProgress(parseFloat((ratio * 100).toFixed(2)));
}, 200);
setUploadSpeed("上传中");
// 转二进制
const binaryData = await uploadFileToBinary(mainFile);
@@ -1324,9 +1360,20 @@ export default function FilesUpload() {
const region = (loaderData.userInfo?.area as string) || "default";
const createdBy = loaderData.userInfo?.user_id as number | undefined;
const uploadResp = await handleFileUpload(
binaryData, mainFile.name, mainFile.type,
fileType as FileType, selectedGroupId ? Number(selectedGroupId) : null, priority,
region, createdBy, attachmentFiles, loaderData.frontendJWT || undefined,
binaryData,
mainFile.name,
mainFile.type,
String(effectiveDocumentTypeId),
effectiveGroupId,
priority,
region,
createdBy,
attachmentFiles,
loaderData.frontendJWT || undefined,
(progress) => {
setUploadProgress(progress.percent);
setUploadSpeed(`${formatFileSize(progress.loaded)}/${formatFileSize(progress.total)}`);
},
);
if (!uploadResp.success) {
@@ -1350,10 +1397,6 @@ export default function FilesUpload() {
}
// 完成:清理进度定时器并置满
if (uploadProgressIntervalRef.current) {
clearInterval(uploadProgressIntervalRef.current);
uploadProgressIntervalRef.current = null;
}
setUploadProgress(100);
setUploadSpeed('完成');
@@ -1378,11 +1421,6 @@ export default function FilesUpload() {
await filterDocuments(documentTypeIds);
} catch (error) {
console.error('合同首传上传失败:', error);
if (uploadProgressIntervalRef.current) {
clearInterval(uploadProgressIntervalRef.current);
uploadProgressIntervalRef.current = null;
}
// 清空合同模板文件缓存
setContractTemplateFiles([]);
console.log('【合同上传失败】已清空合同模板文件缓存');
@@ -1412,7 +1450,7 @@ export default function FilesUpload() {
// 检查是否选择了文件类型
if (!fileType) {
console.error('【调试-checkAndPrepareUpload】未选择文件类型');
toastService.error('请先选择文类型');
toastService.error('请先选择文类型');
return;
}
if (subtypeGroups.length > 1 && !selectedGroupId) {
@@ -1421,7 +1459,7 @@ export default function FilesUpload() {
}
// 检查是否为合同类型
const selectedType = loaderData.documentTypes.find(t => t.id.toString() === fileType);
const selectedType = getSelectedDocumentType();
const isContract = !!(selectedType && selectedType.name.includes('合同'));
// console.log('【调试-checkAndPrepareUpload】文件类型检查', {
@@ -1454,7 +1492,12 @@ export default function FilesUpload() {
// 检查主文件名称是否重复(在任何状态变化之前进行检查)
const mainFile = allFiles[0];
const duplicateResult = await checkDocumentDuplicate(mainFile.name, Number(fileType));
if (!effectiveDocumentTypeId) {
toastService.error('当前子类型缺少可提交的文档类型绑定,请先检查评查点分组配置');
return;
}
const duplicateResult = await checkDocumentDuplicate(mainFile.name, effectiveDocumentTypeId);
if (duplicateResult.is_duplicate) {
const confirmed = window.confirm(
'存在同名文件,会归纳到历史版本中。如需纳入历史版本请点击确定,否则点击取消并重新命名文档后再上传。'
@@ -1505,8 +1548,8 @@ export default function FilesUpload() {
}
}
} else {
console.error('【调试-checkAndPrepareUpload】未选择文类型,无法上传');
toastService.error('请选择文类型');
console.error('【调试-checkAndPrepareUpload】未选择文类型,无法上传');
toastService.error('请选择文类型');
}
} else {
console.error('【调试-checkAndPrepareUpload】没有文件可上传');
@@ -1551,13 +1594,12 @@ export default function FilesUpload() {
setUploadStage("uploading");
setUploadProgress(0);
setUploadSpeed("上传中");
// 计算总文件大小
const totalSize = files.reduce((sum, file) => sum + file.size, 0);
let uploadedSize = 0;
// console.log('【调试-startUpload】总文件大小:', formatFileSize(totalSize));
// 更新步骤状态
const updatedSteps = [...processingSteps];
updatedSteps[0].status = "active";
@@ -1571,33 +1613,6 @@ export default function FilesUpload() {
return;
}
// 转换文件为二进制格式
// console.log("【调试-startUpload】开始转换文件到二进制格式...");
// 模拟上传进度
if (uploadProgressIntervalRef.current) {
clearInterval(uploadProgressIntervalRef.current);
}
const startTime = Date.now();
let lastUploadedSize = 0;
let lastUpdateTime = startTime;
uploadProgressIntervalRef.current = setInterval(() => {
const currentTime = Date.now();
const timeElapsed = (currentTime - lastUpdateTime) / 1000; // 使用最近一次更新的时间间隔
const currentSpeed = timeElapsed > 0 ? (uploadedSize - lastUploadedSize) / timeElapsed : 0; // 字节/秒
lastUploadedSize = uploadedSize;
lastUpdateTime = currentTime;
// 更新上传速度显示
setUploadSpeed(`${formatFileSize(currentSpeed)}/s`);
// 更新进度 - 保留2位小数
const progress = Math.min((uploadedSize / totalSize) * 100, 99.99);
setUploadProgress(parseFloat(progress.toFixed(2)));
}, 200); // 改为200ms更新一次,提供更准确的速度计算
// 上传所有文件
const uploadedFiles: UploadedFile[] = [];
@@ -1629,44 +1644,35 @@ export default function FilesUpload() {
// console.log(`【调试-startUpload】准备上传文件 ${file.name} 到服务器`);
// 创建基于时间的渐进式进度模拟
const startUploadTime = Date.now();
// 根据文件大小动态估算上传时间,考虑网络速度
const estimatedUploadTime = Math.max(
file.size / (1024 * 1024) / 3 * 1000, // 假设3MB/s的速度,1MB需要1/3秒
1000 // 最小1秒
);
let progressInterval: NodeJS.Timeout | null = null;
// 开始渐进式进度更新
const progressPromise = new Promise<void>((resolve) => {
progressInterval = setInterval(() => {
const elapsed = Date.now() - startUploadTime;
const progressRatio = Math.min(elapsed / estimatedUploadTime, 0.95); // 最大95%
// 计算当前文件的进度贡献
const fileProgress = progressRatio * file.size;
const previousFilesSize = files.slice(0, temp_n - 1).reduce((sum, f) => sum + f.size, 0);
uploadedSize = previousFilesSize + fileProgress;
// 如果接近完成,停止进度更新并resolve
if (progressRatio >= 0.95) {
if (progressInterval) {
clearInterval(progressInterval);
progressInterval = null;
}
resolve();
}
}, 100); // 改为100ms更新一次,提供更流畅的进度
});
// 使用Promise.race添加超时处理
const region = (loaderData.userInfo?.area as string) || "default";
const createdBy = loaderData.userInfo?.user_id as number | undefined;
if (!effectiveDocumentTypeId) {
throw new Error("当前子类型缺少可提交的文档类型绑定,请先检查评查点分组配置");
}
const uploadPromise = handleFileUpload(
binaryData, file.name, file.type,
fileType as FileType, selectedGroupId ? Number(selectedGroupId) : null, priority,
region, createdBy, undefined, loaderData.frontendJWT || undefined,
binaryData,
file.name,
file.type,
String(effectiveDocumentTypeId),
effectiveGroupId,
priority,
region,
createdBy,
undefined,
loaderData.frontendJWT || undefined,
(progress) => {
const previousFilesSize = files
.slice(0, temp_n - 1)
.reduce((sum, currentFile) => sum + currentFile.size, 0);
const currentLoaded = Math.min(progress.loaded, file.size);
uploadedSize = previousFilesSize + currentLoaded;
const overallProgress = totalSize > 0 ? (uploadedSize / totalSize) * 100 : 0;
setUploadProgress(parseFloat(Math.min(overallProgress, 100).toFixed(2)));
setUploadSpeed(
`${formatFileSize(previousFilesSize + currentLoaded)}/${formatFileSize(totalSize)}`,
);
},
);
const timeoutPromise = new Promise<UploadResult>((_, reject) => {
@@ -1675,16 +1681,7 @@ export default function FilesUpload() {
}, 600000);
});
// 并行执行上传和进度更新
const [uploadResult] = await Promise.all([
Promise.race([uploadPromise, timeoutPromise]),
progressPromise
]);
// 清除进度定时器
if (progressInterval) {
clearInterval(progressInterval);
}
const uploadResult = await Promise.race([uploadPromise, timeoutPromise]);
// 再次检查组件是否已卸载
if (!isMountedRef.current) {
@@ -1748,10 +1745,6 @@ export default function FilesUpload() {
}
// 清除进度定时器
if (uploadProgressIntervalRef.current) {
clearInterval(uploadProgressIntervalRef.current);
}
// 更新上传状态
setUploadProgress(100);
setUploadSpeed("完成");
@@ -1763,7 +1756,7 @@ export default function FilesUpload() {
return {
id,
name: file.name,
type_id: fileType ? parseInt(fileType) : 0,
type_id: effectiveDocumentTypeId || (fileType ? parseInt(fileType) : 0),
file_size: file.size,
status: DocumentStatus.CUTTING,
created_at: new Date().toISOString()
@@ -1789,11 +1782,6 @@ export default function FilesUpload() {
setProcessingSteps(errorSteps);
// 清除进度定时器
if (uploadProgressIntervalRef.current) {
clearInterval(uploadProgressIntervalRef.current);
}
showFriendlyUploadError(error, { titlePrefix: '文件上传失败' });
resetUpload();
@@ -1824,6 +1812,8 @@ export default function FilesUpload() {
updatedSteps[1].description = "文档正在排队等待处理...";
setProcessingSteps(updatedSteps);
setUploadProgress(mapProcessingStatusToProgress(DocumentStatus.QUEUED));
setUploadSpeed(mapProcessingStatusToSpeed(DocumentStatus.QUEUED));
// 获取文件ID列表
const fileIds = files.map(file => file.id).filter(id => id > 0);
@@ -1925,6 +1915,17 @@ export default function FilesUpload() {
// console.log('【调试-checkProcessingStatus】没有返回文件状态数据');
return;
}
const hasFailed = response.data.some(doc => doc.status === DocumentStatus.FAILED);
if (hasFailed) {
if (processingStatusIntervalRef.current) {
clearInterval(processingStatusIntervalRef.current);
processingStatusIntervalRef.current = null;
}
updateProcessingSteps(DocumentStatus.FAILED);
updateQueueFilesStatus(response.data);
return;
}
// 检查是否所有文件都已完成处理
const allCompleted = response.data.every(doc => doc.status === DocumentStatus.PROCESSED);
@@ -1955,6 +1956,8 @@ export default function FilesUpload() {
completedSteps[5].description = "文档已准备就绪,可以查看";
setProcessingSteps(completedSteps);
setUploadProgress(100);
setUploadSpeed(mapProcessingStatusToSpeed(DocumentStatus.PROCESSED));
setUploadStage("completed");
} else {
// 根据当前状态更新步骤
@@ -2036,9 +2039,18 @@ export default function FilesUpload() {
updatedSteps[5].status = "done";
updatedSteps[5].description = "文档已准备就绪,可以查看";
break;
case DocumentStatus.FAILED:
updatedSteps[1].status = "done";
updatedSteps[1].description = "已进入处理队列";
updatedSteps[2].status = "error";
updatedSteps[2].description = "文档处理失败,请检查上传文件或稍后重试";
break;
}
setProcessingSteps(updatedSteps);
setUploadProgress(mapProcessingStatusToProgress(status));
setUploadSpeed(mapProcessingStatusToSpeed(status));
};
// 更新队列中文件的状态
@@ -2063,12 +2075,6 @@ export default function FilesUpload() {
// 重置上传状态 - 不清除队列状态检查定时器
const resetUpload = () => {
// 清除上传和处理相关的定时器
if (uploadProgressIntervalRef.current) {
clearInterval(uploadProgressIntervalRef.current);
uploadProgressIntervalRef.current = null;
}
if (processingStatusIntervalRef.current) {
clearInterval(processingStatusIntervalRef.current);
processingStatusIntervalRef.current = null;
@@ -2136,7 +2142,11 @@ export default function FilesUpload() {
// 获取文档类型名称
const getDocumentTypeName = (codeId: number) => {
const type = documentTypesState.find(t => t.id === codeId);
return type ? type.name : '未知类型';
if (type) {
return type.name;
}
const matchedRoot = documentTypesState.find((item) => (item.childDocumentTypeIds || []).includes(codeId));
return matchedRoot ? matchedRoot.name : '未知类型';
};
// 处理查看文件
@@ -2390,10 +2400,10 @@ export default function FilesUpload() {
{/* 文件类型选择和上传表单 */}
<Form method="post" encType="multipart/form-data" ref={formRef}>
{/* 文件类型选择 */}
<Card title={<h3></h3>} className="mb-4">
<Card title={<h3></h3>} className="mb-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="form-group">
<label htmlFor="file-type-select" className="form-label"> <span className="text-red-500">*</span></label>
<label htmlFor="file-type-select" className="form-label"> <span className="text-red-500">*</span></label>
<select
id="file-type-select"
name="fileType"
@@ -2402,7 +2412,7 @@ export default function FilesUpload() {
onChange={handleFileTypeChange}
disabled={uploadStage !== "idle"}
>
<option value=""></option>
<option value=""></option>
{documentTypesState.map(type => (
<option key={type.id} value={type.id}>{type.name}</option>
))}
@@ -2413,7 +2423,7 @@ export default function FilesUpload() {
<div className="text-red-500 text-sm mt-1">{fileTypeError}</div>
)}
<div className="form-tip"></div>
<div className="form-tip"></div>
</div>
<div className="form-group">
<label htmlFor="priority-select" className="form-label"></label>
@@ -2446,11 +2456,11 @@ export default function FilesUpload() {
required={subtypeGroups.length > 1}
>
{!fileType ? (
<option value=""></option>
<option value=""></option>
) : groupOptionsLoading ? (
<option value="">...</option>
) : subtypeGroups.length === 0 ? (
<option value=""></option>
<option value=""></option>
) : subtypeGroups.length === 1 ? (
<option value={String(subtypeGroups[0].id)}>
{getSubtypeDisplayName(subtypeGroups[0])}
@@ -2468,27 +2478,35 @@ export default function FilesUpload() {
</select>
<div className="form-tip">
{!fileType
? "请先选择文件类型,再确定本次上传实际命中的子类型。"
? "请先选择一级文档类型,再确定本次上传实际命中的子类型。"
: groupOptionsLoading
? "正在加载当前文档类型下可用的子类型配置。"
? "正在加载当前一级文档类型下可用的子类型配置。"
: subtypeGroups.length === 0
? "当前文档类型在当前入口下还没有可用子类型,请先到评查点分组管理补齐“一级分组 / 二级分组 / 规则集”绑定。"
? "当前一级文档类型在当前入口下还没有可用子类型,请先到评查点分组管理补齐“一级分组 / 二级分组 / 规则集”绑定。"
: hasMultipleSubtypeGroups
? "同一文档类型在当前入口下已拆分多个子类型,请选择本次上传实际命中的子类型。"
? "当前一级文档类型在当前入口下已拆分多个子类型,请选择本次上传实际命中的子类型。"
: isSingleDefaultSubtype
? "当前文档类型在当前入口下尚未拆分业务子类型,系统将按默认子类型处理;后续可在评查点分组管理中继续细分。"
: "当前文档类型在当前入口下仅配置了个子类型,系统自动带出该子类型。"}
? "当前一级文档类型在当前入口下尚未拆分业务子类型,系统将按默认子类型处理;后续可在评查点分组管理中继续细分。"
: "当前一级文档类型在当前入口下仅配置了 1 个子类型,系统自动带出。"}
</div>
{selectedRootGroupName ? (
<div className="form-tip">
{selectedRootGroupName}
{selectedEntryModuleName ? ` · 入口模块:${selectedEntryModuleName}` : ""}
</div>
) : null}
{selectedSubtypeGroup ? (
{selectedEntryModuleName ? (
<div className="form-tip">
{getSubtypeDisplayName(selectedSubtypeGroup)}
{selectedSubtypeGroup.displayHint ? ` · ${selectedSubtypeGroup.displayHint}` : ""}
{selectedEntryModuleName}
</div>
) : null}
{effectiveSubtypeGroup ? (
<div className="form-tip">
{getSubtypeDisplayName(effectiveSubtypeGroup)}
</div>
) : null}
{effectiveSubtypeGroup?.code ? (
<div className="form-tip">
{effectiveSubtypeGroup.code}
</div>
) : null}
</div>
@@ -2802,7 +2820,7 @@ export default function FilesUpload() {
fileName={`${currentFiles.length}个文件`}
fileSize={formatFileSize(currentFiles.reduce((sum, file) => sum + file.size, 0))}
progress={uploadProgress}
speed={''}
speed={uploadSpeed}
/>
)}
@@ -2867,10 +2885,6 @@ export default function FilesUpload() {
type="default"
icon="ri-refresh-line"
onClick={() => {
// 清除所有定时器
if (uploadProgressIntervalRef.current) {
clearInterval(uploadProgressIntervalRef.current);
}
if (processingStatusIntervalRef.current) {
clearInterval(processingStatusIntervalRef.current);
}