初版编写交叉评查页面,增加非范围日期选择器
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState, useRef } from "react";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { type MetaFunction, type ActionFunctionArgs } from "@remix-run/node";
|
||||
import { Form, useNavigation } from "@remix-run/react";
|
||||
import { UploadArea, type UploadAreaRef } from "~/components/ui/UploadArea";
|
||||
@@ -6,6 +6,8 @@ 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,
|
||||
@@ -14,6 +16,7 @@ import {
|
||||
formatFileSize,
|
||||
batchUploadCrossCheckingFiles
|
||||
} from "~/api/cross-checking/cross-files-upload";
|
||||
import React from "react"; // Added for React.useState
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
@@ -28,7 +31,8 @@ export const handle = {
|
||||
|
||||
export function links() {
|
||||
return [
|
||||
{ rel: "stylesheet", href: crossCheckingUploadStyles }
|
||||
{ rel: "stylesheet", href: crossCheckingUploadStyles },
|
||||
...dateRangePickerLinks()
|
||||
];
|
||||
}
|
||||
|
||||
@@ -39,6 +43,152 @@ const STEPS = [
|
||||
{ id: 3, label: "选择卷宗" }
|
||||
];
|
||||
|
||||
// 1. TreeNode类型和MOCK_TREE
|
||||
export interface TreeNode {
|
||||
label: string;
|
||||
value: string;
|
||||
children?: TreeNode[];
|
||||
}
|
||||
|
||||
const MOCK_TREE: TreeNode[] = [
|
||||
{
|
||||
label: "梅州市",
|
||||
value: "梅州市",
|
||||
children: [
|
||||
{
|
||||
label: "梅江区",
|
||||
value: "梅江区",
|
||||
children: [
|
||||
{
|
||||
label: "梅江区烟草局",
|
||||
value: "梅江区烟草局",
|
||||
children: [
|
||||
{ label: "张三", value: "梅州市-梅江区-梅江区烟草局-张三" },
|
||||
{ label: "李四", value: "梅州市-梅江区-梅江区烟草局-李四" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "揭阳市",
|
||||
value: "揭阳市",
|
||||
children: [
|
||||
{
|
||||
label: "榕城区",
|
||||
value: "榕城区",
|
||||
children: [
|
||||
{
|
||||
label: "榕城区烟草局",
|
||||
value: "榕城区烟草局",
|
||||
children: [
|
||||
{ label: "王五", value: "揭阳市-榕城区-榕城区烟草局-王五" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// 2. TreeMultiSelect递归组件
|
||||
function getAllLeafValues(node: TreeNode): string[] {
|
||||
if (!node.children || node.children.length === 0) return [node.value];
|
||||
return node.children.flatMap(getAllLeafValues);
|
||||
}
|
||||
function getAllLeafValuesFromTree(tree: TreeNode[]): string[] {
|
||||
return tree.flatMap(getAllLeafValues);
|
||||
}
|
||||
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" }}
|
||||
>
|
||||
{expanded ? "▼" : "▶"}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
const TreeMultiSelect: React.FC<{
|
||||
treeData: TreeNode[];
|
||||
value: string[];
|
||||
onChange: (v: string[]) => void;
|
||||
}> = ({ treeData, value, onChange }) => {
|
||||
// 递归处理选中/取消
|
||||
const handleCheck = (node: TreeNode, checked: boolean) => {
|
||||
const leafValues = getAllLeafValues(node);
|
||||
let newValue: string[];
|
||||
if (checked) {
|
||||
newValue = Array.from(new Set([...value, ...leafValues]));
|
||||
} else {
|
||||
newValue = value.filter(v => !leafValues.includes(v));
|
||||
}
|
||||
onChange(newValue);
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
{treeData.map(node => (
|
||||
<TreeNodeCheckbox
|
||||
key={node.value}
|
||||
node={node}
|
||||
checked={value}
|
||||
onCheck={handleCheck}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
const formData = await request.formData();
|
||||
const caseType = formData.get("caseType") as string;
|
||||
@@ -54,8 +204,16 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
export default function CrossCheckingUpload() {
|
||||
// 基础状态
|
||||
const [caseType, setCaseType] = useState<CaseType>(CaseType.ADMINISTRATIVE_PENALTY);
|
||||
const [currentStep] = useState(1);
|
||||
const navigation = useNavigation();
|
||||
// 步骤状态
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
// 步骤1:任务信息
|
||||
const [taskInfo, setTaskInfo] = useState({
|
||||
name: '',
|
||||
date: '',
|
||||
type: '市局交叉评查',
|
||||
});
|
||||
// 步骤2状态
|
||||
const [groupChecked, setGroupChecked] = useState<string[]>([]);
|
||||
|
||||
// 上传配置状态 - 设置默认值
|
||||
const [priority] = useState<string>("normal");
|
||||
@@ -299,8 +457,20 @@ export default function CrossCheckingUpload() {
|
||||
}
|
||||
};
|
||||
|
||||
// 步骤切换
|
||||
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();
|
||||
// 小组多选逻辑
|
||||
useEffect(() => {
|
||||
setGroupChecked(getAllLeafValuesFromTree(MOCK_TREE));
|
||||
}, []);
|
||||
|
||||
// 检查是否可以完成
|
||||
const canComplete = (singleFiles.length > 0 || multipleFiles.length > 0) && !isUploading;
|
||||
const navigation = useNavigation();
|
||||
const isSubmitting = navigation.state === "submitting";
|
||||
|
||||
return (
|
||||
@@ -309,13 +479,11 @@ export default function CrossCheckingUpload() {
|
||||
{/* 步骤指示器 */}
|
||||
<div className="steps-indicator">
|
||||
{STEPS.map((step) => (
|
||||
<div
|
||||
key={step.id}
|
||||
<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-circle ${step.id === currentStep ? 'active' : 'inactive'}`}>{step.id}</div>
|
||||
<div className="step-label">{step.label}</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -343,179 +511,251 @@ export default function CrossCheckingUpload() {
|
||||
</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>
|
||||
{uploadType === 'single' && singleFiles.length > 0 && (
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
icon="ri-delete-bin-line"
|
||||
onClick={() => handleClearFiles('single')}
|
||||
disabled={isUploading}
|
||||
>
|
||||
清空
|
||||
</Button>
|
||||
)}
|
||||
</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}
|
||||
{/* 步骤1:创建任务 */}
|
||||
{currentStep === 1 && (
|
||||
<div className="step-form-container">
|
||||
<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="请输入任务名称"
|
||||
/>
|
||||
|
||||
{/* 单案件文件列表 */}
|
||||
{singleFiles.length > 0 && (
|
||||
<div className="mt-4 space-y-2">
|
||||
<div className="text-sm font-medium text-gray-700">
|
||||
已选择 {singleFiles.length} 个文件:
|
||||
</div>
|
||||
<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-gray-50 p-2 rounded">
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 多案件导入 */}
|
||||
<div className="upload-item">
|
||||
<div className="upload-item-header">
|
||||
<i className="upload-item-icon ri-file-list-line"></i>
|
||||
<span>多案件导入</span>
|
||||
{uploadType === 'multiple' && multipleFiles.length > 0 && (
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
icon="ri-delete-bin-line"
|
||||
onClick={() => handleClearFiles('multiple')}
|
||||
disabled={isUploading}
|
||||
>
|
||||
清空
|
||||
</Button>
|
||||
)}
|
||||
</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">
|
||||
请上传多个案件作为压缩包zip、rar、7z、tar文件
|
||||
</div>
|
||||
}
|
||||
disabled={uploadType === 'single' || isUploading}
|
||||
<div className="form-group">
|
||||
<label className="form-label required">评查时间</label>
|
||||
<SingleDatePicker
|
||||
date={taskInfo.date}
|
||||
onDateChange={(value) => setTaskInfo({ ...taskInfo, date: value })}
|
||||
className="w-full"
|
||||
id="task-date"
|
||||
placeholder="请选择日期"
|
||||
/>
|
||||
|
||||
{/* 多案件文件列表 */}
|
||||
{multipleFiles.length > 0 && (
|
||||
<div className="mt-4 space-y-2">
|
||||
<div className="text-sm font-medium text-gray-700">
|
||||
已选择 {multipleFiles.length} 个压缩包:
|
||||
</div>
|
||||
<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-gray-50 p-2 rounded">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* 完成按钮 */}
|
||||
<div className="complete-button-container">
|
||||
<button
|
||||
type="button"
|
||||
className="complete-button"
|
||||
disabled={!canComplete}
|
||||
onClick={handleCompleteUpload}
|
||||
>
|
||||
{isUploading || isSubmitting ? (
|
||||
<>
|
||||
<i className="ri-loader-4-line animate-spin mr-2"></i>
|
||||
上传中...
|
||||
</>
|
||||
) : (
|
||||
"完成"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
{/* 文件选择状态提示 */}
|
||||
{!canComplete && !isUploading && (
|
||||
<div className="text-center mt-4 text-gray-500 text-sm">
|
||||
请至少选择一种导入方式的文件
|
||||
<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-center mt-8">
|
||||
<Button type="primary" disabled={!canNextStep1} onClick={handleNext}>下一步</Button>
|
||||
</div>
|
||||
</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 animate-spin text-xl mr-2"></i>
|
||||
<span className="font-medium">正在上传文件...</span>
|
||||
{/* 步骤2:创建评查小组 */}
|
||||
{currentStep === 2 && (
|
||||
<>
|
||||
<div className="step-form-container flex flex-row justify-center items-start gap-12">
|
||||
{/* 左侧树状多选 */}
|
||||
<div style={{ minWidth: 260 }}>
|
||||
<div className="form-group">
|
||||
<label className="form-label required">评查小组</label>
|
||||
<MultiCascader
|
||||
options={MOCK_TREE}
|
||||
placeholder="请选择评查小组"
|
||||
defaultValue={groupChecked}
|
||||
onChange={(values: string[]) => {
|
||||
setGroupChecked(values);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-blue-700">
|
||||
正在上传 {uploadType === 'single' ? singleFiles.length : multipleFiles.length} 个文件,请稍候
|
||||
</p>
|
||||
{/* 右侧预留区域 */}
|
||||
<div style={{ minWidth: 320, minHeight: 200, background: '#fff', border: '1.5px solid #e5e7eb', borderRadius: 8 }}></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-center mt-8 space-x-4">
|
||||
<Button type="default" onClick={handlePrev}>上一步</Button>
|
||||
<Button type="primary" disabled={groupChecked.length === 0} onClick={handleNext}>下一步</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 步骤3:原有上传区域 */}
|
||||
{currentStep === 3 && (
|
||||
<>
|
||||
{/* 文件上传区域 */}
|
||||
<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>
|
||||
{uploadType === 'single' && singleFiles.length > 0 && (
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
icon="ri-delete-bin-line"
|
||||
onClick={() => handleClearFiles('single')}
|
||||
disabled={isUploading}
|
||||
>
|
||||
清空
|
||||
</Button>
|
||||
)}
|
||||
</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}
|
||||
/>
|
||||
|
||||
{/* 单案件文件列表 */}
|
||||
{singleFiles.length > 0 && (
|
||||
<div className="mt-4 space-y-2">
|
||||
<div className="text-sm font-medium text-gray-700">
|
||||
已选择 {singleFiles.length} 个文件:
|
||||
</div>
|
||||
<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-gray-50 p-2 rounded">
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 多案件导入 */}
|
||||
<div className="upload-item">
|
||||
<div className="upload-item-header">
|
||||
<i className="upload-item-icon ri-file-list-line"></i>
|
||||
<span>多案件导入</span>
|
||||
{uploadType === 'multiple' && multipleFiles.length > 0 && (
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
icon="ri-delete-bin-line"
|
||||
onClick={() => handleClearFiles('multiple')}
|
||||
disabled={isUploading}
|
||||
>
|
||||
清空
|
||||
</Button>
|
||||
)}
|
||||
</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">
|
||||
请上传多个案件作为压缩包zip、rar、7z、tar文件
|
||||
</div>
|
||||
}
|
||||
disabled={uploadType === 'single' || isUploading}
|
||||
/>
|
||||
|
||||
{/* 多案件文件列表 */}
|
||||
{multipleFiles.length > 0 && (
|
||||
<div className="mt-4 space-y-2">
|
||||
<div className="text-sm font-medium text-gray-700">
|
||||
已选择 {multipleFiles.length} 个压缩包:
|
||||
</div>
|
||||
<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-gray-50 p-2 rounded">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* 完成按钮 */}
|
||||
<div className="complete-button-container">
|
||||
<button
|
||||
type="button"
|
||||
className="complete-button"
|
||||
disabled={!canComplete}
|
||||
onClick={handleCompleteUpload}
|
||||
>
|
||||
{isUploading || isSubmitting ? (
|
||||
<>
|
||||
<i className="ri-loader-4-line animate-spin mr-2"></i>
|
||||
上传中...
|
||||
</>
|
||||
) : (
|
||||
"完成"
|
||||
)}
|
||||
</button>
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user