1012 lines
48 KiB
TypeScript
1012 lines
48 KiB
TypeScript
import { useState, useRef, useEffect } from "react";
|
|
import { type MetaFunction, type ActionFunctionArgs, type LoaderFunctionArgs, json } from "@remix-run/node";
|
|
import { Form, useNavigate, useLoaderData } 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>
|
|
);
|
|
};
|
|
/**
|
|
* 获取用户会话和前端JWT
|
|
*/
|
|
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|
// 获取用户会话信息
|
|
const { getUserSession } = await import("~/api/login/auth.server");
|
|
const { userInfo, frontendJWT } = await getUserSession(request);
|
|
|
|
return json({
|
|
userInfo,
|
|
frontendJWT
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 创建交叉评查任务
|
|
* @param taskData 任务数据
|
|
* @param token JWT Token
|
|
* @returns 创建结果
|
|
*/
|
|
async function createCrossReviewTask(taskData: {
|
|
documentIds: number[];
|
|
userIds: number[];
|
|
assignerId: number;
|
|
taskName: string;
|
|
}, token: string) {
|
|
try {
|
|
const response = await fetch('/admin/crossreview/tasks/assign', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`
|
|
},
|
|
body: JSON.stringify({
|
|
document_ids: taskData.documentIds,
|
|
user_ids: taskData.userIds,
|
|
assigner_id: taskData.assignerId,
|
|
task_name: taskData.taskName
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
console.log('任务创建成功:', result);
|
|
return result;
|
|
} catch (error) {
|
|
console.error('创建任务失败:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
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() {
|
|
// 获取loader数据
|
|
const { userInfo, frontendJWT } = useLoaderData<typeof loader>();
|
|
|
|
// 基础状态
|
|
const [caseType, setCaseType] = useState<CaseType>(CaseType.ADMINISTRATIVE_PENALTY);
|
|
// 步骤状态
|
|
const [currentStep, setCurrentStep] = useState(1);
|
|
// 任务创建状态
|
|
const [isCreatingTask, setIsCreatingTask] = useState(false);
|
|
// 步骤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);
|
|
|
|
|
|
|
|
// 处理案卷类型切换
|
|
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 handleCreateTask = async () => {
|
|
// 验证步骤1:任务信息
|
|
if (!taskInfo.name.trim()) {
|
|
toastService.error("请填写任务名称");
|
|
return;
|
|
}
|
|
if (!taskInfo.date.trim()) {
|
|
toastService.error("请选择任务日期");
|
|
return;
|
|
}
|
|
|
|
// 验证步骤2:评查小组
|
|
if (groupChecked.length === 0) {
|
|
toastService.error("请选择评查小组成员");
|
|
return;
|
|
}
|
|
|
|
// 验证步骤3:文件上传
|
|
const filesToUpload = uploadType === 'single' ? singleFiles : multipleFiles;
|
|
if (filesToUpload.length === 0) {
|
|
toastService.error("请先选择要上传的文件");
|
|
return;
|
|
}
|
|
|
|
setIsCreatingTask(true);
|
|
setIsUploading(true);
|
|
|
|
try {
|
|
// 第一步:上传文件
|
|
console.log("开始批量上传文件:", filesToUpload.length, "个,案卷类型:", caseType);
|
|
|
|
const uploadResult = await batchUploadCrossCheckingFiles(
|
|
filesToUpload.map(f => f.file),
|
|
caseType,
|
|
priority,
|
|
isTestDocument,
|
|
frontendJWT
|
|
);
|
|
|
|
const { successes, failures } = uploadResult;
|
|
|
|
if (failures.length > 0) {
|
|
toastService.error(`文件上传失败:${failures[0].error}`);
|
|
return;
|
|
}
|
|
|
|
// 第二步:创建交叉评查任务
|
|
console.log("文件上传成功,开始创建任务");
|
|
|
|
// 提取文档ID
|
|
const documentIds = successes.map(success => success.result.result?.id).filter(id => id !== undefined) as number[];
|
|
|
|
// 提取用户ID(从选中的组织架构中获取用户)
|
|
const userIds = groupChecked.filter(id => {
|
|
// 检查是否为用户ID(通常用户ID以特定前缀开头或有特定格式)
|
|
return id.includes('user_');
|
|
}).map(id => parseInt(id.replace('user_', '')));
|
|
|
|
if (userIds.length === 0) {
|
|
toastService.error("请选择具体的评查人员");
|
|
return;
|
|
}
|
|
|
|
// 创建任务数据
|
|
const taskData = {
|
|
documentIds,
|
|
userIds,
|
|
assignerId: userInfo?.user_id || 1, // 使用当前用户ID作为分配者
|
|
taskName: taskInfo.name
|
|
};
|
|
|
|
console.log("创建任务数据:", taskData);
|
|
|
|
// 调用创建任务接口
|
|
await createCrossReviewTask(taskData, frontendJWT);
|
|
|
|
// 任务创建成功
|
|
toastService.success("交叉评查任务创建成功!");
|
|
messageService.success(
|
|
`任务创建完成!\n任务名称:${taskInfo.name}\n上传文件:${successes.length} 个\n评查人员:${userIds.length} 人`,
|
|
{
|
|
title: '任务创建成功',
|
|
confirmText: '确定',
|
|
onConfirm: () => {
|
|
// 跳转到任务列表页面
|
|
navigate('/cross-checking');
|
|
}
|
|
}
|
|
);
|
|
|
|
} catch (error) {
|
|
console.error("创建任务失败:", error);
|
|
toastService.error(`创建任务失败:${error instanceof Error ? error.message : '未知错误'}`);
|
|
messageService.error(`创建任务失败:${error instanceof Error ? error.message : '未知错误'}`, {
|
|
title: '创建失败',
|
|
confirmText: '确定',
|
|
});
|
|
} finally {
|
|
setIsCreatingTask(false);
|
|
setIsUploading(false);
|
|
}
|
|
};
|
|
|
|
// 处理完成上传(保留原有功能用于测试)
|
|
// 处理完成上传(保留原有功能用于测试)
|
|
// const handleCompleteUpload = async () => {
|
|
// const filesToUpload = uploadType === 'single' ? singleFiles : multipleFiles;
|
|
|
|
// if (filesToUpload.length === 0) {
|
|
// toastService.error("请先选择要上传的文件");
|
|
// return;
|
|
// }
|
|
|
|
// setIsUploading(true);
|
|
|
|
// try {
|
|
// console.log("开始批量上传文件:", filesToUpload.length, "个,案卷类型:", caseType);
|
|
|
|
// const result = await batchUploadCrossCheckingFiles(
|
|
// filesToUpload.map(f => f.file),
|
|
// caseType,
|
|
// priority,
|
|
// isTestDocument,
|
|
// frontendJWT
|
|
// );
|
|
|
|
// 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();
|
|
// 由于 isSubmitting 未被使用,暂时移除该行代码
|
|
|
|
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('开始加载组织架构数据');
|
|
// 传递JWT token到API调用
|
|
const response = await getOrganizationTree(true, frontendJWT);
|
|
|
|
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">
|
|
请上传多个案件作为压缩包zip、rar、7z、tar文件
|
|
</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 || isCreatingTask}
|
|
onClick={handleCreateTask}
|
|
>
|
|
{isCreatingTask ? "创建任务中..." : isUploading ? "上传中..." : "开始创建任务"}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Form>
|
|
|
|
{/* 文件选择状态提示 */}
|
|
{!canComplete && !isUploading && (
|
|
<div className="text-center mt-4 text-gray-500 text-sm">
|
|
请至少选择一种导入方式的文件
|
|
</div>
|
|
)}
|
|
|
|
{/* 创建任务进度提示 */}
|
|
{(isUploading || isCreatingTask) && (
|
|
<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">
|
|
{isCreatingTask ? "正在创建任务..." : "正在上传文件..."}
|
|
</span>
|
|
</div>
|
|
<p className="text-sm text-blue-700">
|
|
{isCreatingTask
|
|
? `正在创建交叉评查任务:${taskInfo.name}`
|
|
: `正在上传 ${uploadType === 'single' ? singleFiles.length : multipleFiles.length} 个文件,请稍候`
|
|
}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
} |