Files
leaudit-platform-frontend/app/routes/cross-checking.upload.tsx
T
TanWenyan 4d5ec6cdb7 接入feat(cross-checking): 整合组织架构数据并优化意见列表功能
- 更新 API 配置,使用新的后端服务地址- 移除前端模拟数据,改为从后端获取真实数据- 优化意见列表接口,支持分页和用户身份验证
- 调整前端界面,适应新的数据结构和功能需求
2025-07-20 21:29:42 +08:00

842 lines
41 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useRef, useEffect } from "react";
import { type MetaFunction, type ActionFunctionArgs } from "@remix-run/node";
import { Form, useNavigation, useNavigate } from "@remix-run/react";
import { UploadArea, type UploadAreaRef } from "~/components/ui/UploadArea";
import { Button } from "~/components/ui/Button";
import { messageService } from "~/components/ui/MessageModal";
import { toastService } from "~/components/ui/Toast";
import crossCheckingUploadStyles from "~/styles/pages/cross-checking-upload.css?url";
import MultiCascader from "~/components/ui/MultiCascader";
import { SingleDatePicker, links as dateRangePickerLinks } from "~/components/ui/DateRangePicker";
import {
CaseType,
CASE_TYPE_TO_TYPE_ID,
type CrossCheckingUploadedFile,
generateFileId,
formatFileSize,
batchUploadCrossCheckingFiles
} from "~/api/cross-checking/cross-files-upload";
import {
getOrganizationTree,
convertToTreeData
} from "~/api/user";
import React from "react"; // Added for React.useState
export const meta: MetaFunction = () => {
return [
{ title: "交叉评查上传 - 中国烟草AI合同及卷宗审核系统" },
{ name: "description", content: "交叉评查案卷上传和任务创建" }
];
};
export const handle = {
breadcrumb: "交叉评查上传"
};
export function links() {
return [
{ rel: "stylesheet", href: crossCheckingUploadStyles },
...dateRangePickerLinks()
];
}
// 步骤枚举
const STEPS = [
{ id: 1, label: "创建任务" },
{ id: 2, label: "创建评查小组" },
{ id: 3, label: "选择卷宗" }
];
// 1. TreeNode类型和MOCK_TREE
export interface TreeNode {
label: string;
value: string;
children?: TreeNode[];
}
// 默认的空组织架构数据(作为备用)
const DEFAULT_TREE: TreeNode[] = [];
// 用户选择状态管理
interface UserSelectionState {
treeData: TreeNode[];
loading: boolean;
error: string | null;
}
function isAllChildrenChecked(node: TreeNode, checked: string[]): boolean {
if (!node.children || node.children.length === 0) return checked.includes(node.value);
return node.children.every(child => isAllChildrenChecked(child, checked));
}
function isSomeChildrenChecked(node: TreeNode, checked: string[]): boolean {
if (!node.children || node.children.length === 0) return checked.includes(node.value);
return node.children.some(child => isSomeChildrenChecked(child, checked));
}
const TreeNodeCheckbox: React.FC<{
node: TreeNode;
checked: string[];
onCheck: (node: TreeNode, checked: boolean) => void;
level?: number;
}> = ({ node, checked, onCheck, level = 0 }) => {
const [expanded, setExpanded] = React.useState(true);
const allChecked = isAllChildrenChecked(node, checked);
const someChecked = isSomeChildrenChecked(node, checked);
const isLeaf = !node.children || node.children.length === 0;
return (
<div style={{ marginLeft: level * 18 }}>
<div className="flex items-center">
{!isLeaf && (
<span
className="mr-1 cursor-pointer select-none"
onClick={() => setExpanded(e => !e)}
role="button"
tabIndex={0}
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && setExpanded(e => !e)}
style={{ width: 16, display: "inline-block", textAlign: "center" }}
>
<i className={expanded ? "ri-arrow-down-s-line" : "ri-arrow-right-s-line"}></i>
</span>
)}
<input
type="checkbox"
className="form-checkbox"
checked={allChecked}
ref={el => { if (el) el.indeterminate = !allChecked && someChecked; }}
onChange={e => onCheck(node, e.target.checked)}
id={node.value}
/>
<label htmlFor={node.value} className="ml-2">{node.label}</label>
</div>
{expanded && node.children && (
<div>
{node.children.map(child => (
<TreeNodeCheckbox
key={child.value}
node={child}
checked={checked}
onCheck={onCheck}
level={level + 1}
/>
))}
</div>
)}
</div>
);
};
export const action = async ({ request }: ActionFunctionArgs) => {
const formData = await request.formData();
const caseType = formData.get("caseType") as string;
const uploadType = formData.get("uploadType") as string;
console.log("交叉评查上传:", { caseType, uploadType });
// 这里可以处理上传后的业务逻辑
// 例如创建任务记录等
return Response.json({ success: true, message: "文件上传成功" });
};
export default function CrossCheckingUpload() {
// 基础状态
const [caseType, setCaseType] = useState<CaseType>(CaseType.ADMINISTRATIVE_PENALTY);
// 步骤状态
const [currentStep, setCurrentStep] = useState(1);
// 步骤1:任务信息
const [taskInfo, setTaskInfo] = useState({
name: '',
date: '',
type: '市局交叉评查',
});
// 步骤2状态
const [groupChecked, setGroupChecked] = useState<string[]>([]);
const [userSelectionState, setUserSelectionState] = useState<UserSelectionState>({
treeData: DEFAULT_TREE,
loading: false,
error: null
});
// 上传配置状态 - 设置默认值
const [priority] = useState<string>("normal");
const [documentNumber] = useState<string>("");
const [remark] = useState<string>("");
const [isTestDocument] = useState<boolean>(false);
// 文件管理状态
const [singleFiles, setSingleFiles] = useState<CrossCheckingUploadedFile[]>([]);
const [multipleFiles, setMultipleFiles] = useState<CrossCheckingUploadedFile[]>([]);
const [uploadType, setUploadType] = useState<'none' | 'single' | 'multiple'>('none');
const [isUploading, setIsUploading] = useState(false);
// 引用
const singleUploadRef = useRef<UploadAreaRef>(null);
const multipleUploadRef = useRef<UploadAreaRef>(null);
// 获取当前typeId
const currentTypeId = CASE_TYPE_TO_TYPE_ID[caseType];
// 处理案卷类型切换
const handleCaseTypeChange = (type: CaseType) => {
if (isUploading) {
toastService.warning("上传进行中,无法切换案卷类型");
return;
}
setCaseType(type);
// 清空已选择的文件和重置上传方式
clearAllFiles();
console.log("案卷类型切换为:", type, "typeId:", CASE_TYPE_TO_TYPE_ID[type]);
};
// 清空所有文件
const clearAllFiles = () => {
setSingleFiles([]);
setMultipleFiles([]);
setUploadType('none');
// 重置文件输入框
singleUploadRef.current?.resetFileInput();
multipleUploadRef.current?.resetFileInput();
};
// 处理单案件文件选择
const handleSingleFilesSelected = (files: FileList) => {
if (uploadType === 'multiple') {
toastService.warning("已选择多案件导入方式,无法选择单案件文件");
return;
}
const validFiles: CrossCheckingUploadedFile[] = [];
let hasInvalidFiles = false;
Array.from(files).forEach(file => {
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
validFiles.push({
id: generateFileId(),
file,
name: file.name,
size: file.size,
type: file.type,
uploadType: 'single'
});
} else {
hasInvalidFiles = true;
}
});
if (hasInvalidFiles) {
messageService.error('只能上传PDF格式的文件', {
title: '文件类型错误',
confirmText: '确定',
});
}
if (validFiles.length > 0) {
setSingleFiles(prev => [...prev, ...validFiles]);
setUploadType('single');
console.log("选择单案件文件:", validFiles.length, "个");
}
};
// 处理多案件文件选择
const handleMultipleFilesSelected = (files: FileList) => {
if (uploadType === 'single') {
toastService.warning("已选择单案件导入方式,无法选择多案件文件");
return;
}
const validFiles: CrossCheckingUploadedFile[] = [];
let hasInvalidFiles = false;
Array.from(files).forEach(file => {
const isZip = file.type === 'application/zip' ||
file.type === 'application/x-zip-compressed' ||
file.name.toLowerCase().endsWith('.zip');
const isRar = file.type === 'application/x-rar-compressed' ||
file.name.toLowerCase().endsWith('.rar');
const is7z = file.type === 'application/x-7z-compressed' ||
file.name.toLowerCase().endsWith('.7z');
const isTar = file.type === 'application/x-tar' ||
file.name.toLowerCase().endsWith('.tar');
if (isZip || isRar || is7z || isTar) {
validFiles.push({
id: generateFileId(),
file,
name: file.name,
size: file.size,
type: file.type,
uploadType: 'multiple'
});
} else {
hasInvalidFiles = true;
}
});
if (hasInvalidFiles) {
messageService.error('只能上传ZIP或RAR格式的压缩文件', {
title: '文件类型错误',
confirmText: '确定',
});
}
if (validFiles.length > 0) {
setMultipleFiles(prev => [...prev, ...validFiles]);
setUploadType('multiple');
console.log("选择多案件文件:", validFiles.length, "个");
}
};
// 删除单个文件
const handleRemoveFile = (fileId: string, type: 'single' | 'multiple') => {
if (isUploading) {
toastService.warning("上传进行中,无法删除文件");
return;
}
if (type === 'single') {
setSingleFiles(prev => {
const newFiles = prev.filter(f => f.id !== fileId);
if (newFiles.length === 0) {
setUploadType('none');
singleUploadRef.current?.resetFileInput();
}
return newFiles;
});
} else {
setMultipleFiles(prev => {
const newFiles = prev.filter(f => f.id !== fileId);
if (newFiles.length === 0) {
setUploadType('none');
multipleUploadRef.current?.resetFileInput();
}
return newFiles;
});
}
};
// 清空文件列表
const handleClearFiles = (type: 'single' | 'multiple') => {
if (isUploading) {
toastService.warning("上传进行中,无法清空文件");
return;
}
if (type === 'single') {
setSingleFiles([]);
singleUploadRef.current?.resetFileInput();
} else {
setMultipleFiles([]);
multipleUploadRef.current?.resetFileInput();
}
setUploadType('none');
};
// 处理完成上传
const handleCompleteUpload = async () => {
const filesToUpload = uploadType === 'single' ? singleFiles : multipleFiles;
if (filesToUpload.length === 0) {
toastService.error("请先选择要上传的文件");
return;
}
setIsUploading(true);
try {
console.log("开始批量上传文件:", filesToUpload.length, "个,案卷类型:", caseType, "typeId:", currentTypeId);
const result = await batchUploadCrossCheckingFiles(
filesToUpload,
currentTypeId,
priority,
documentNumber,
remark,
isTestDocument
);
const { successes, failures } = result;
if (failures.length === 0) {
// 全部成功
toastService.success(`成功上传 ${successes.length} 个文件`);
// 立即清空文件列表
clearAllFiles();
messageService.success(`文件上传完成!成功上传 ${successes.length} 个文件,现在可以进行下一步操作。`, {
title: '上传成功',
confirmText: '确定'
});
} else if (successes.length === 0) {
// 全部失败
toastService.error(`文件上传失败,共 ${failures.length} 个文件上传失败`);
messageService.error(`所有文件上传失败。失败原因:${failures[0].error}`, {
title: '上传失败',
confirmText: '确定',
});
} else {
// 部分成功
toastService.warning(`部分文件上传成功:成功 ${successes.length} 个,失败 ${failures.length}`);
messageService.warning(
`部分文件上传完成:\n成功:${successes.length} 个文件\n失败:${failures.length} 个文件\n\n失败文件:\n${failures.map(f => `${f.file.name}: ${f.error}`).join('\n')}`,
{
title: '部分上传成功',
confirmText: '确定',
}
);
}
} catch (error) {
console.error("批量上传失败:", error);
toastService.error("文件上传过程中发生错误");
messageService.error(`文件上传失败:${error instanceof Error ? error.message : '未知错误'}`, {
title: '上传失败',
confirmText: '确定',
});
} finally {
setIsUploading(false);
}
};
// 步骤切换
const handleNext = () => setCurrentStep((s) => Math.min(s + 1, 3));
const handlePrev = () => setCurrentStep((s) => Math.max(s - 1, 1));
// 步骤1表单校验
const canNextStep1 = taskInfo.name.trim() && taskInfo.date.trim() && taskInfo.type.trim();
// 小组多选逻辑 - 默认不选择任何项
// 检查是否可以完成
const canComplete = (singleFiles.length > 0 || multipleFiles.length > 0) && !isUploading;
const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting";
const navigate = useNavigate();
// 加载组织架构数据
useEffect(() => {
const loadOrganizationData = async () => {
// 只在步骤2且数据为空且未在加载时执行
if (currentStep === 2 && userSelectionState.treeData.length === 0 && !userSelectionState.loading) {
setUserSelectionState(prev => ({ ...prev, loading: true, error: null }));
try {
console.log('开始加载组织架构数据');
const response = await getOrganizationTree(true);
if (response.success && response.data) {
console.log('原始API数据:', response.data);
const treeData = convertToTreeData(response.data.organizations);
console.log('转换后的树形数据:', treeData);
// 验证数据转换是否正确
treeData.forEach(org => {
console.log(`组织: ${org.label} (${org.value})`);
if (org.children) {
org.children.forEach(child => {
if (child.isUser) {
console.log(` - 用户: ${child.label} (${child.value})`);
} else {
console.log(` - 子组织: ${child.label} (${child.value})`);
}
});
}
});
setUserSelectionState({
treeData,
loading: false,
error: null
});
} else {
console.error('获取组织架构失败:', response.error);
setUserSelectionState({
treeData: DEFAULT_TREE,
loading: false,
error: response.error || '获取组织架构失败'
});
toastService.error('获取组织架构失败,请刷新页面重试');
}
} catch (error) {
console.error('加载组织架构数据失败:', error);
setUserSelectionState({
treeData: DEFAULT_TREE,
loading: false,
error: error instanceof Error ? error.message : '加载组织架构数据失败'
});
toastService.error('加载组织架构数据失败,请刷新页面重试');
}
}
};
loadOrganizationData();
}, [currentStep]); // 只依赖 currentStep,避免无限循环
return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="max-w-6xl mx-auto px-4">
{/* 步骤指示器 */
<div className="steps-indicator">
{STEPS.map((step) => (
<div
key={step.id}
className={`step-item ${step.id < currentStep ? 'completed' : ''}`}
>
<div className={`step-circle ${step.id === currentStep ? 'active' : 'inactive'}`}>{step.id}</div>
<div className="step-label">{step.label}</div>
</div>
))}
</div>
}
{/* 步骤1:创建任务 */}
{currentStep === 1 && (
<div className="flex justify-center">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6" style={{width: '1000px'}}>
<div className="form-group">
<label htmlFor="task-name"><span className="text-red-500">*</span></label>
<input
id="task-name"
className="form-input"
value={taskInfo.name}
onChange={e => setTaskInfo({ ...taskInfo, name: e.target.value })}
placeholder="请输入任务名称"
/>
</div>
<div className="form-group">
<label htmlFor="task-date" className="form-label required"></label>
<SingleDatePicker
date={taskInfo.date}
onDateChange={(value) => setTaskInfo({ ...taskInfo, date: value })}
className="w-full"
id="task-date"
placeholder="请选择日期"
/>
</div>
<div className="form-group">
<label htmlFor="task-type"></label>
<input
id="task-type"
className="form-input"
value={taskInfo.type}
onChange={e => setTaskInfo({ ...taskInfo, type: e.target.value })}
placeholder="请输入任务类型"
/>
</div>
<div className="flex justify-between items-center mt-6">
<Button
type="default"
icon="ri-arrow-left-line"
onClick={() => {
console.log('点击返回列表按钮');
navigate('/cross-checking');
}}
>
</Button>
<Button type="primary" disabled={!canNextStep1} onClick={handleNext}></Button>
</div>
</div>
</div>
)}
{/* 步骤2:创建评查小组 */}
{currentStep === 2 && (
<>
<div className="flex justify-center">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6" style={{width: '1000px'}}>
<div className="flex flex-row justify-center gap-12">
{/* 左侧树状多选 */}
<div style={{ minWidth: 300, width: '40%' }}>
<div className="form-group">
<label htmlFor="review-group" className="form-label required"></label>
{userSelectionState.loading ? (
<div className="flex items-center justify-center p-4 border border-gray-200 rounded-md bg-gray-50">
<i className="ri-loader-4-line ri-spin animate-spin text-xl mr-2 text-blue-600"></i>
<span className="text-gray-600">...</span>
</div>
) : userSelectionState.error ? (
<div className="flex items-center justify-center p-4 border border-red-200 rounded-md bg-red-50">
<i className="ri-error-warning-line text-xl mr-2 text-red-600"></i>
<span className="text-red-600">: {userSelectionState.error}</span>
</div>
) : (
<MultiCascader
options={userSelectionState.treeData}
placeholder="请选择评查小组成员"
value={groupChecked}
onChange={(values: string[]) => {
setGroupChecked(values);
}}
/>
)}
</div>
</div>
{/* 右侧已选择成员显示区域 */}
<div style={{ minWidth: 400, width: '50%', minHeight: 250, background: '#f9fafb', border: '1.5px solid #e5e7eb', borderRadius: 8, marginTop: '23px' }}>
<div className="p-4">
<h4 className="text-sm font-medium text-gray-700 mb-3"></h4>
{groupChecked.length > 0 ? (
<div className="space-y-2 max-h-64 overflow-y-auto">
{groupChecked.map((member, index) => {
// 处理用户选择值,支持新的API格式
let displayName = member;
let displayOrg = '';
if (member.startsWith('user_')) {
// 用户选择,格式为 user_123
displayName = `用户ID: ${member.replace('user_', '')}`;
displayOrg = '用户';
} else {
// 组织选择,格式为 ou_id 或 ou_id-ou_id
const parts = member.split('-');
displayName = parts[parts.length - 1];
displayOrg = parts.slice(0, -1).join(' - ') || '组织';
}
return (
<div key={index} className="bg-white p-2 rounded text-xs border">
<div className="font-medium text-gray-800">{displayName}</div>
<div className="text-gray-500 mt-1">{displayOrg}</div>
</div>
);
})}
</div>
) : (
<div className="text-gray-400 text-sm text-center mt-8">
<i className="ri-user-line text-2xl mb-2 block"></i>
</div>
)}
<div className="mt-4 pt-3 border-t border-gray-200">
<div className="text-xs text-gray-500">
{groupChecked.length}
</div>
</div>
</div>
</div>
</div>
{/* 按钮区域移到卡片内部 */}
<div className="flex justify-between items-center mt-6">
<Button
type="default"
icon="ri-arrow-left-line"
onClick={() => {
console.log('点击返回列表按钮');
navigate('/cross-checking');
}}
>
</Button>
<div className="flex space-x-4">
<Button type="default" onClick={handlePrev}></Button>
<Button type="primary" disabled={groupChecked.length === 0} onClick={handleNext}></Button>
</div>
</div>
</div>
</div>
</>
)}
{/* 步骤3:原有上传区域 */}
{currentStep === 3 && (
<div className="flex justify-center">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6" style={{width: '1000px'}}>
{/* 案卷类型选择器 */}
<div className="flex justify-center mb-6">
<div>
<div className="text-sm font-medium text-gray-700 mb-3 text-center"></div>
<div className="case-type-options">
<button
type="button"
className={`case-type-option ${caseType === CaseType.ADMINISTRATIVE_PENALTY ? 'active' : 'inactive'}`}
onClick={() => handleCaseTypeChange(CaseType.ADMINISTRATIVE_PENALTY)}
disabled={isUploading}
>
</button>
<button
type="button"
className={`case-type-option ${caseType === CaseType.ADMINISTRATIVE_PERMIT ? 'active' : 'inactive'}`}
onClick={() => handleCaseTypeChange(CaseType.ADMINISTRATIVE_PERMIT)}
disabled={isUploading}
>
</button>
</div>
</div>
</div>
{/* 文件上传区域 */}
<Form method="post" encType="multipart/form-data">
<input type="hidden" name="caseType" value={caseType} />
<input type="hidden" name="uploadType" value={uploadType} />
{/* 上传框区域 */}
<div className="upload-section">
{/* 单案件导入 */}
<div className="upload-item">
<div className="upload-item-header">
<i className="upload-item-icon ri-file-text-line"></i>
<span></span>
</div>
<UploadArea
ref={singleUploadRef}
onFilesSelected={handleSingleFilesSelected}
className="custom-upload-area"
accept=".pdf"
multiple={true}
icon="ri-file-upload-line"
buttonText="选择文件"
mainText="点击或拖拽文件到此区域上传"
tipText={
<div className="upload-tip-error">
PDF文件
</div>
}
disabled={uploadType === 'multiple' || isUploading}
/>
</div>
{/* 多案件导入 */}
<div className="upload-item">
<div className="upload-item-header">
<i className="upload-item-icon ri-file-list-line"></i>
<span></span>
</div>
<UploadArea
ref={multipleUploadRef}
onFilesSelected={handleMultipleFilesSelected}
className="custom-upload-area"
accept=".zip,.rar,.7z,.tar"
multiple={false}
icon="ri-folder-zip-line"
buttonText="选择文件"
mainText="点击或拖拽文件到此区域上传"
tipText={
<div className="upload-tip-error">
ziprar7ztar文件
</div>
}
disabled={uploadType === 'single' || isUploading}
/>
</div>
</div>
{/* 文件预览区域 */}
{(singleFiles.length > 0 || multipleFiles.length > 0) && (
<div className="mt-6 bg-green-50 border border-green-200 rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<div className="text-sm font-medium text-gray-700">
{uploadType === 'single' ? singleFiles.length : multipleFiles.length}
</div>
<Button
type="default"
size="small"
icon="ri-delete-bin-line"
onClick={() => handleClearFiles(uploadType === 'single' ? 'single' : 'multiple')}
disabled={isUploading}
>
</Button>
</div>
{/* 单案件文件列表 */}
{uploadType === 'single' && singleFiles.length > 0 && (
<div className="max-h-32 overflow-y-auto space-y-1">
{singleFiles.map((file) => (
<div key={file.id} className="flex items-center justify-between bg-white p-2 rounded border">
<div className="flex items-center space-x-2 flex-1 min-w-0">
<i className="ri-file-pdf-line text-red-500"></i>
<span className="text-sm truncate">{file.name}</span>
<span className="text-xs text-gray-500">{formatFileSize(file.size)}</span>
</div>
<button
type="button"
onClick={() => handleRemoveFile(file.id, 'single')}
className="text-red-500 hover:text-red-700 p-1"
disabled={isUploading}
>
<i className="ri-close-line"></i>
</button>
</div>
))}
</div>
)}
{/* 多案件文件列表 */}
{uploadType === 'multiple' && multipleFiles.length > 0 && (
<div className="max-h-32 overflow-y-auto space-y-1">
{multipleFiles.map((file) => (
<div key={file.id} className="flex items-center justify-between bg-white p-2 rounded border">
<div className="flex items-center space-x-2 flex-1 min-w-0">
<i className="ri-folder-zip-line text-orange-500"></i>
<span className="text-sm truncate">{file.name}</span>
<span className="text-xs text-gray-500">{formatFileSize(file.size)}</span>
</div>
<button
type="button"
onClick={() => handleRemoveFile(file.id, 'multiple')}
className="text-red-500 hover:text-red-700 p-1"
disabled={isUploading}
>
<i className="ri-close-line"></i>
</button>
</div>
))}
</div>
)}
</div>
)}
{/* 完成按钮 */}
<div className="flex justify-between items-center mt-8">
<Button
type="default"
icon="ri-arrow-left-line"
onClick={() => {
console.log('点击返回列表按钮');
navigate('/cross-checking');
}}
>
</Button>
<div className="flex space-x-4">
<Button type="default" onClick={handlePrev}></Button>
<Button
type="primary"
disabled={!canComplete || isUploading}
onClick={handleCompleteUpload}
>
{isUploading || isSubmitting ? "上传中..." : "开始创建任务"}
</Button>
</div>
</div>
</Form>
{/* 文件选择状态提示 */}
{!canComplete && !isUploading && (
<div className="text-center mt-4 text-gray-500 text-sm">
</div>
)}
{/* 上传进度提示 */}
{isUploading && (
<div className="text-center mt-4">
<div className="bg-blue-50 p-4 rounded-md border border-blue-100">
<div className="flex items-center justify-center text-blue-800 mb-2">
<i className="ri-loader-4-line ri-spin animate-spin text-xl mr-2"></i>
<span className="font-medium">...</span>
</div>
<p className="text-sm text-blue-700">
{uploadType === 'single' ? singleFiles.length : multipleFiles.length}
</p>
</div>
</div>
)}
</div>
</div>
)}
</div>
</div>
);
}