feat: wire real upload progress and subtype mapping
This commit is contained in:
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user