feat: stabilize document type and upload flows
This commit is contained in:
+169
-38
@@ -11,6 +11,7 @@ import uploadStyles from "~/styles/pages/files_upload.css?url";
|
||||
import { messageService } from "~/components/ui/MessageModal";
|
||||
import { toastService } from "~/components/ui/Toast";
|
||||
import {
|
||||
buildUploadErrorDetails,
|
||||
getTodayDocuments,
|
||||
getDocumentTypes,
|
||||
getDocumentsStatus,
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
checkDocumentDuplicate,
|
||||
type Document,
|
||||
type DocumentType,
|
||||
type UploadErrorDetails,
|
||||
type UploadResult,
|
||||
DocumentStatus
|
||||
} from "~/api/files/files-upload";
|
||||
@@ -119,6 +121,11 @@ export interface UploadedFile {
|
||||
};
|
||||
}
|
||||
|
||||
type UploadRequestError = Error & {
|
||||
status?: number;
|
||||
payload?: unknown;
|
||||
};
|
||||
|
||||
// 修改文件上传函数部分,解决类型问题
|
||||
async function handleFileUpload(
|
||||
binaryData: ArrayBuffer,
|
||||
@@ -147,7 +154,14 @@ async function handleFileUpload(
|
||||
if ("error" in response || !response.data) {
|
||||
const errMsg = "error" in response ? response.error : "上传响应为空";
|
||||
console.error("上传失败:", errMsg);
|
||||
throw new Error(errMsg || "上传失败");
|
||||
const uploadError = new Error(errMsg || "上传失败") as UploadRequestError;
|
||||
if ("status" in response) {
|
||||
uploadError.status = response.status;
|
||||
}
|
||||
if ("payload" in response) {
|
||||
uploadError.payload = response.payload;
|
||||
}
|
||||
throw uploadError;
|
||||
}
|
||||
|
||||
return response.data;
|
||||
@@ -369,41 +383,123 @@ export default function FilesUpload() {
|
||||
// 队列文件状态
|
||||
const [queueFiles, setQueueFiles] = useState<Document[]>([]);
|
||||
const [documentTypesState, setDocumentTypesState] = useState<DocumentType[]>([]);
|
||||
const [uploadErrorDetails, setUploadErrorDetails] = useState<UploadErrorDetails | null>(null);
|
||||
|
||||
// 全局队列状态(用于显示排队统计)
|
||||
const [globalQueueStatus, setGlobalQueueStatus] = useState<QueueStatus | null>(null);
|
||||
|
||||
const getSelectedDocumentType = () =>
|
||||
documentTypesState.find(type => type.id.toString() === fileType) ||
|
||||
loaderData.documentTypes.find(type => type.id.toString() === fileType) ||
|
||||
null;
|
||||
|
||||
const clearUploadErrorDetails = () => {
|
||||
setUploadErrorDetails(null);
|
||||
};
|
||||
|
||||
const showFriendlyUploadError = (
|
||||
error: unknown,
|
||||
options?: {
|
||||
titlePrefix?: string;
|
||||
resetAfterClose?: boolean;
|
||||
}
|
||||
) => {
|
||||
const uploadError = error instanceof Error ? (error as UploadRequestError) : null;
|
||||
const rawMessage = uploadError?.message || (typeof error === 'string' ? error : '未知错误');
|
||||
const details = buildUploadErrorDetails(rawMessage, {
|
||||
documentType: getSelectedDocumentType(),
|
||||
status: uploadError?.status,
|
||||
payload: (uploadError?.payload as Record<string, unknown> | null | undefined),
|
||||
});
|
||||
|
||||
setUploadErrorDetails(details);
|
||||
|
||||
const detailPreview = details.detailLines.slice(0, 2).join('\n');
|
||||
const actionPreview = details.actionLines[0] ? `\n\n建议:${details.actionLines[0]}` : '';
|
||||
const modalMessage = `${details.summary}${detailPreview ? `\n\n${detailPreview}` : ''}${actionPreview}`;
|
||||
|
||||
messageService.error(modalMessage, {
|
||||
title: options?.titlePrefix ? `${options.titlePrefix} - ${details.title}` : details.title,
|
||||
confirmText: '确定',
|
||||
cancelText: '',
|
||||
onConfirm: options?.resetAfterClose ? () => resetUpload() : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
// 在组件挂载时从 sessionStorage 获取 documentTypeIds
|
||||
useEffect(() => {
|
||||
try {
|
||||
// 在客户端环境中执行
|
||||
if (typeof window !== 'undefined') {
|
||||
const typeIdsStr = sessionStorage.getItem('documentTypeIds');
|
||||
const typeIds = typeIdsStr ? JSON.parse(typeIdsStr) : null;
|
||||
setDocumentTypeIds(typeIds);
|
||||
// 根据 documentTypeIds 过滤文档类型和文档列表
|
||||
filterDocumentTypes(typeIds, loaderData.documentTypes);
|
||||
filterDocuments(typeIds);
|
||||
let cancelled = false;
|
||||
|
||||
// 如果包含合同类型(ID=1),自动选择合同文档类型
|
||||
if (typeIds && typeIds.includes(1)) {
|
||||
setIsContractType(true);
|
||||
// 查找ID为1的合同文档类型
|
||||
const contractType = loaderData.documentTypes.find(type => type.id === 1);
|
||||
if (contractType) {
|
||||
setFileType(contractType.id.toString());
|
||||
// 清除可能存在的文件类型错误
|
||||
setFileTypeError(null);
|
||||
try {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const bootstrapUploadScope = async () => {
|
||||
const typeIdsStr = sessionStorage.getItem('documentTypeIds');
|
||||
const cachedTypeIds = typeIdsStr ? JSON.parse(typeIdsStr) : null;
|
||||
const selectedModuleIdStr = sessionStorage.getItem('selectedModuleId');
|
||||
const nextSelectedModuleId = selectedModuleIdStr ? Number(selectedModuleIdStr) : null;
|
||||
const normalizedModuleId = Number.isFinite(nextSelectedModuleId) && (nextSelectedModuleId || 0) > 0
|
||||
? nextSelectedModuleId
|
||||
: null;
|
||||
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
let scopedTypes = loaderData.documentTypes;
|
||||
let effectiveTypeIds = cachedTypeIds;
|
||||
|
||||
// 客户端可读取 sessionStorage,因此这里重新按入口模块拉一次,避免 SSR 阶段拿到全量类型后继续走旧缓存。
|
||||
if (normalizedModuleId) {
|
||||
const scopedTypesResponse = await getDocumentTypes(loaderData.frontendJWT || undefined);
|
||||
if (!cancelled && !scopedTypesResponse.error && scopedTypesResponse.data) {
|
||||
scopedTypes = scopedTypesResponse.data;
|
||||
effectiveTypeIds = scopedTypes.map(type => type.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDocumentTypeIds(effectiveTypeIds);
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
setIsContractType(false);
|
||||
}
|
||||
};
|
||||
|
||||
void bootstrapUploadScope().catch(error => {
|
||||
console.error('初始化上传页入口模块作用域失败:', error);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取 sessionStorage 中的 documentTypeIds 失败:', error);
|
||||
}
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [loaderData]);
|
||||
|
||||
// 过滤文档类型列表
|
||||
const filterDocumentTypes = (documentTypeIds: number[] | null, types: DocumentType[]) => {
|
||||
const filterDocumentTypes = (documentTypeIds: number[] | null, types: DocumentType[], selectedModuleId?: number | null) => {
|
||||
if (selectedModuleId) {
|
||||
// 已通过 entry_module_id 从后端取过当前入口模块的文档类型,前端不再二次用旧缓存裁剪
|
||||
setDocumentTypesState(types);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!documentTypeIds || documentTypeIds.length === 0) {
|
||||
// 如果没有特定的 documentTypeIds,使用原始数据
|
||||
setDocumentTypesState(types);
|
||||
@@ -439,7 +535,7 @@ export default function FilesUpload() {
|
||||
setQueueFiles(loaderData.documents);
|
||||
return;
|
||||
}
|
||||
const documents = response.data || [];
|
||||
const documents = (response.data || []).filter(doc => documentTypeIds.includes(doc.type_id));
|
||||
console.log('过滤文档列表成功:', documents);
|
||||
setQueueFiles(documents);
|
||||
|
||||
@@ -591,11 +687,6 @@ export default function FilesUpload() {
|
||||
try {
|
||||
// console.log('开始检查队列状态,当前队列文件:', files);
|
||||
|
||||
// 直接从sessionStorage读取documentTypeIds,避免异步状态更新问题
|
||||
const typeIdsStr = typeof window !== 'undefined' ? sessionStorage.getItem('documentTypeIds') : null;
|
||||
const currentDocumentTypeIds = typeIdsStr ? JSON.parse(typeIdsStr) : null;
|
||||
// console.log('从sessionStorage读取的documentTypeIds:', currentDocumentTypeIds);
|
||||
|
||||
// 获取所有未完成的文档
|
||||
const incompleteFiles = files.filter(file =>
|
||||
file.status !== DocumentStatus.PROCESSED && file.id
|
||||
@@ -609,7 +700,6 @@ export default function FilesUpload() {
|
||||
let statusResponse;
|
||||
|
||||
// 如果是合同类型(ID=1),需要分类处理
|
||||
// console.log('当前documentTypeIds:', currentDocumentTypeIds);
|
||||
// if (currentDocumentTypeIds && currentDocumentTypeIds.includes(1)) {
|
||||
// // 分类文档ID
|
||||
// const mainDocumentIds: number[] = [];
|
||||
@@ -666,6 +756,7 @@ export default function FilesUpload() {
|
||||
// 处理文件选择
|
||||
const handleFilesSelected = (files: FileList) => {
|
||||
if (files.length > 0) {
|
||||
clearUploadErrorDetails();
|
||||
// 验证文件类型,支持PDF和Word文件
|
||||
const validFiles: File[] = [];
|
||||
let hasInvalidFiles = false;
|
||||
@@ -708,6 +799,7 @@ export default function FilesUpload() {
|
||||
// 处理文件类型变化
|
||||
const handleFileTypeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const value = e.target.value;
|
||||
clearUploadErrorDetails();
|
||||
// 确保只有选择了有效的文件类型才进行设置
|
||||
if (value) {
|
||||
// console.log('【调试-handleFileTypeChange】文件类型变更为:', value);
|
||||
@@ -1102,6 +1194,7 @@ export default function FilesUpload() {
|
||||
// 合同专用:首传即合并的上传链路
|
||||
const startContractUpload = async (mainFiles: File[], attachmentFiles: File[], templateFiles: File[] = []) => {
|
||||
try {
|
||||
clearUploadErrorDetails();
|
||||
// 只允许一个主文件
|
||||
const mainFile = mainFiles[0];
|
||||
|
||||
@@ -1220,7 +1313,6 @@ export default function FilesUpload() {
|
||||
await filterDocuments(documentTypeIds);
|
||||
} catch (error) {
|
||||
console.error('合同首传上传失败:', error);
|
||||
messageService.error(`合同上传失败:${error instanceof Error ? error.message : '未知错误'}`);
|
||||
if (uploadProgressIntervalRef.current) {
|
||||
clearInterval(uploadProgressIntervalRef.current);
|
||||
uploadProgressIntervalRef.current = null;
|
||||
@@ -1230,6 +1322,7 @@ export default function FilesUpload() {
|
||||
setContractTemplateFiles([]);
|
||||
console.log('【合同上传失败】已清空合同模板文件缓存');
|
||||
|
||||
showFriendlyUploadError(error, { titlePrefix: '合同上传失败' });
|
||||
resetUpload();
|
||||
}
|
||||
};
|
||||
@@ -1237,6 +1330,7 @@ export default function FilesUpload() {
|
||||
// 检查并准备上传
|
||||
const checkAndPrepareUpload = async (mainFiles: File[], attachmentFiles: File[], templateFiles: File[] = []) => {
|
||||
try {
|
||||
clearUploadErrorDetails();
|
||||
// console.log('【调试-checkAndPrepareUpload】开始检查并准备上传文件', {
|
||||
// mainFilesCount: mainFiles.length,
|
||||
// attachmentFilesCount: attachmentFiles.length,
|
||||
@@ -1362,6 +1456,7 @@ export default function FilesUpload() {
|
||||
// 开始上传文件
|
||||
const startUpload = async (files: File[]) => {
|
||||
try {
|
||||
clearUploadErrorDetails();
|
||||
console.log('【调试-startUpload】开始上传过程,文件数量:', files.length);
|
||||
|
||||
// 检查组件是否已卸载
|
||||
@@ -1630,15 +1725,7 @@ export default function FilesUpload() {
|
||||
clearInterval(uploadProgressIntervalRef.current);
|
||||
}
|
||||
|
||||
// 显示错误提示
|
||||
messageService.error(`文件上传失败:${error instanceof Error ? error.message : '未知错误'}`, {
|
||||
title: '文件上传失败',
|
||||
confirmText: '确定',
|
||||
cancelText: '',
|
||||
onConfirm: () => {
|
||||
resetUpload();
|
||||
}
|
||||
});
|
||||
showFriendlyUploadError(error, { titlePrefix: '文件上传失败' });
|
||||
resetUpload();
|
||||
|
||||
// 抛出错误,让React错误边界捕获并显示
|
||||
@@ -2330,6 +2417,50 @@ export default function FilesUpload() {
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{uploadErrorDetails && (
|
||||
<Card className="mb-4 upload-error-card">
|
||||
<div className="upload-error-banner">
|
||||
<div className="upload-error-banner__header">
|
||||
<div className="upload-error-banner__title-wrap">
|
||||
<i className="ri-error-warning-line upload-error-banner__icon"></i>
|
||||
<div>
|
||||
<div className="upload-error-banner__title">{uploadErrorDetails.title}</div>
|
||||
<p className="upload-error-banner__summary">{uploadErrorDetails.summary}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="upload-error-banner__dismiss"
|
||||
onClick={clearUploadErrorDetails}
|
||||
aria-label="关闭上传失败提示"
|
||||
>
|
||||
<i className="ri-close-line"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="upload-error-banner__content">
|
||||
<div>
|
||||
<div className="upload-error-banner__label">定位信息</div>
|
||||
<ul className="upload-error-banner__list">
|
||||
{uploadErrorDetails.detailLines.map((line, index) => (
|
||||
<li key={`detail-${index}`}>{line}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="upload-error-banner__label">建议处理</div>
|
||||
<ol className="upload-error-banner__list upload-error-banner__list--ordered">
|
||||
{uploadErrorDetails.actionLines.map((line, index) => (
|
||||
<li key={`action-${index}`}>{line}</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 文件上传区域 */}
|
||||
<Card className="mb-4">
|
||||
|
||||
Reference in New Issue
Block a user