import React, { useState, useRef, useEffect } from "react"; import { type MetaFunction, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node"; import { 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, batchUploadAndAssignCrossCheckingFiles } from "~/api/cross-checking/cross-files-upload"; import { getOrganizationTree, convertToTreeData } from "~/api/user"; import { API_BASE_URL } from '~/config/api-config'; 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 (
{!isLeaf && ( 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" }} > )} { if (el) el.indeterminate = !allChecked && someChecked; }} onChange={e => onCheck(node, e.target.checked)} id={node.value} />
{expanded && node.children && (
{node.children.map(child => ( ))}
)}
); }; /** * 获取用户会话和前端JWT */ export const loader = async ({ request }: LoaderFunctionArgs) => { // 获取用户会话信息 const { getUserSession } = await import("~/api/login/auth.server"); const { userInfo, frontendJWT } = await getUserSession(request); return Response.json({ userInfo, frontendJWT }); }; /** * 创建交叉评查任务 * @param taskData 任务数据 * @param token JWT Token * @returns 创建结果 */ export async function createCrossReviewTask(taskData: { documentIds: number[]; userIds: number[]; assignerId: number; taskName: string; docType: string; }, token: string) { try { const response = await fetch(`${API_BASE_URL}/admin/cross_review/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, doc_type: taskData.docType }) }); 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(); // 基础状态 const [caseType, setCaseType] = useState(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(userInfo?.user_id ? [`user_${userInfo.user_id}`] : []); const [userSelectionState, setUserSelectionState] = useState({ treeData: DEFAULT_TREE, loading: false, error: null }); // 上传配置状态 - 设置默认值 const [priority] = useState("normal"); const [documentNumber] = useState(""); const [remark] = useState(""); const [isTestDocument] = useState(false); // 文件管理状态 const [singleFiles, setSingleFiles] = useState([]); const [multipleFiles, setMultipleFiles] = useState([]); const [uploadType, setUploadType] = useState<'none' | 'single' | 'multiple'>('none'); const [isUploading, setIsUploading] = useState(false); // 引用 const singleUploadRef = useRef(null); const multipleUploadRef = useRef(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 is7z = file.type === 'application/x-7z-compressed' || file.name.toLowerCase().endsWith('.7z'); if (isZip || is7z) { validFiles.push({ id: generateFileId(), file, name: file.name, size: file.size, type: file.type, uploadType: 'multiple' }); } else { hasInvalidFiles = true; } }); if (hasInvalidFiles) { messageService.error('只能上传ZIP或7Z格式的压缩文件', { 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); // 提取用户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 docTypeMap = { [CaseType.ADMINISTRATIVE_PENALTY]: 'XZCF', [CaseType.ADMINISTRATIVE_PERMIT]: 'XZXK' }; const uploadResult = await batchUploadAndAssignCrossCheckingFiles( filesToUpload, CASE_TYPE_TO_TYPE_ID[caseType], priority, documentNumber, remark, isTestDocument, userIds, taskInfo.name, docTypeMap[caseType] || 'XZCF', frontendJWT ); const { successes, failures } = uploadResult; if (failures.length > 0) { toastService.error(`文件上传或任务分配失败:${failures[0].error}`); return; } // 任务创建成功 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,避免无限循环 // 在 CrossCheckingUpload 组件内添加工具函数 function findUserNameById(tree: TreeNode[], userId: string): string | null { for (const node of tree) { if (node.value === userId && (node as { isUser?: boolean }).isUser) { return node.label; } if (node.children) { const found = findUserNameById(node.children, userId); if (found) return found; } } return null; } return (
{/* 步骤指示器 */}
{STEPS.map((step) => (
{step.id}
{step.label}
))}
{/* 步骤1:创建任务 */} {currentStep === 1 && (
setTaskInfo({ ...taskInfo, name: e.target.value })} placeholder="请输入任务名称" />
setTaskInfo({ ...taskInfo, date: value })} className="w-full" id="task-date" placeholder="请选择日期" />
)} {/* 步骤2:创建评查小组 */} {currentStep === 2 && ( <>
{/* 左侧树状多选 */}
{userSelectionState.loading ? (
正在加载组织架构...
) : userSelectionState.error ? (
加载失败: {userSelectionState.error}
) : ( { setGroupChecked(values); }} /> )}
{/* 右侧已选择成员显示区域 */}

已选择的评查小组成员

{groupChecked.length > 0 ? (
{groupChecked.map((member, index) => { let displayName: string = member; let displayOrg = ''; if (member.startsWith('user_')) { // 查找真实用户名 const userName = findUserNameById(userSelectionState.treeData, member) || member.replace('user_', ''); displayName = userName; displayOrg = '用户'; } else { // 组织 const parts = member.split('-'); displayName = parts[parts.length - 1]; displayOrg = parts.slice(0, -1).join(' - ') || '组织'; } return (
{displayName}
{displayOrg}
); })}
) : (
暂未选择评查小组成员
)}
共选择 {groupChecked.length} 名成员
{/* 按钮区域移到卡片内部 */}
)} {/* 步骤3:原有上传区域 */} {currentStep === 3 && (
{/* 案卷类型选择器 */}
选择案卷类型
{/* 文件上传区域 */} {/* 上传框区域 */}
{/* 单案件导入 */}
单案件导入
请上传案件相关PDF文件
} disabled={uploadType === 'multiple' || isUploading} />
{/* 多案件导入 */}
多案件导入
请上传多个案件作为压缩包zip、7z文件
} disabled={uploadType === 'single' || isUploading} />
{/* 文件预览区域 */} {(singleFiles.length > 0 || multipleFiles.length > 0) && (
已选择 {uploadType === 'single' ? singleFiles.length : multipleFiles.length} 个文件
{/* 单案件文件列表 */} {uploadType === 'single' && singleFiles.length > 0 && (
{singleFiles.map((file) => (
{file.name} {formatFileSize(file.size)}
))}
)} {/* 多案件文件列表 */} {uploadType === 'multiple' && multipleFiles.length > 0 && (
{multipleFiles.map((file) => (
{file.name} {formatFileSize(file.size)}
))}
)}
)} {/* 完成按钮 */}
{/* 文件选择状态提示 */} {!canComplete && !isUploading && (
请至少选择一种导入方式的文件
)} {/* 创建任务进度提示 */} {(isUploading || isCreatingTask) && (
{isCreatingTask ? "正在创建任务..." : "正在上传文件..."}

{isCreatingTask ? `正在创建交叉评查任务:${taskInfo.name}` : `正在上传 ${uploadType === 'single' ? singleFiles.length : multipleFiles.length} 个文件,请稍候` }

)}
)} ); }