import { useState, useRef } 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 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 MOCK_TREE: TreeNode[] = [
{
label: "梅州市",
value: "梅州市",
children: [
{
label: "梅州市烟草局", // 市级局
value: "梅州市烟草局",
children: [
{ label: "李局长", value: "梅州市-梅州市烟草局-李局长" },
{ label: "王副局长", value: "梅州市-梅州市烟草局-王副局长" },
{
label: "市场监管科", // 市级局下的科室
value: "梅州市烟草局-市场监管科",
children: [
{ label: "张科长", value: "梅州市-梅州市烟草局-市场监管科-张科长" },
{ label: "陈主任", value: "梅州市-梅州市烟草局-市场监管科-陈主任" }
]
},
{
label: "法规科",
value: "梅州市烟草局-法规科",
children: [
{ label: "刘科长", value: "梅州市-梅州市烟草局-法规科-刘科长" },
{ label: "周专员", value: "梅州市-梅州市烟草局-法规科-周专员" }
]
}
]
},
{
label: "梅江区", // 区级
value: "梅江区",
children: [
{
label: "梅江区烟草分局", // 区级分局
value: "梅江区烟草分局",
children: [
{ label: "张分局长", value: "梅州市-梅江区-梅江区烟草分局-张分局长" },
{ label: "李副分局长", value: "梅州市-梅江区-梅江区烟草分局-李副分局长" },
{
label: "执法大队", // 分局下的大队
value: "梅江区烟草分局-执法大队",
children: [
{ label: "王队长", value: "梅州市-梅江区-梅江区烟草分局-执法大队-王队长" },
{ label: "陈副队长", value: "梅州市-梅江区-梅江区烟草分局-执法大队-陈副队长" },
{
label: "第一中队", // 大队下的中队
value: "梅江区烟草分局-执法大队-第一中队",
children: [
{ label: "赵中队长", value: "梅州市-梅江区-梅江区烟草分局-执法大队-第一中队-赵中队长" },
{ label: "孙执法员", value: "梅州市-梅江区-梅江区烟草分局-执法大队-第一中队-孙执法员" },
{ label: "钱执法员", value: "梅州市-梅江区-梅江区烟草分局-执法大队-第一中队-钱执法员" }
]
},
{
label: "第二中队",
value: "梅江区烟草分局-执法大队-第二中队",
children: [
{ label: "吴中队长", value: "梅州市-梅江区-梅江区烟草分局-执法大队-第二中队-吴中队长" },
{ label: "郑执法员", value: "梅州市-梅江区-梅江区烟草分局-执法大队-第二中队-郑执法员" }
]
}
]
},
{
label: "办公室",
value: "梅江区烟草分局-办公室",
children: [
{ label: "林主任", value: "梅州市-梅江区-梅江区烟草分局-办公室-林主任" },
{ label: "黄秘书", value: "梅州市-梅江区-梅江区烟草分局-办公室-黄秘书" }
]
}
]
},
{
label: "梅江区市场监管局",
value: "梅江区市场监管局",
children: [
{ label: "刘局长", value: "梅州市-梅江区-梅江区市场监管局-刘局长" },
{ label: "周副局长", value: "梅州市-梅江区-梅江区市场监管局-周副局长" },
{
label: "执法监察科",
value: "梅江区市场监管局-执法监察科",
children: [
{ label: "谢科长", value: "梅州市-梅江区-梅江区市场监管局-执法监察科-谢科长" },
{ label: "何专员", value: "梅州市-梅江区-梅江区市场监管局-执法监察科-何专员" }
]
}
]
}
]
},
{
label: "梅县区", // 另一个区
value: "梅县区",
children: [
{
label: "梅县区烟草分局",
value: "梅县区烟草分局",
children: [
{ label: "黄分局长", value: "梅州市-梅县区-梅县区烟草分局-黄分局长" },
{ label: "林副分局长", value: "梅州市-梅县区-梅县区烟草分局-林副分局长" },
{
label: "稽查队",
value: "梅县区烟草分局-稽查队",
children: [
{ label: "吴队长", value: "梅州市-梅县区-梅县区烟草分局-稽查队-吴队长" },
{ label: "郑稽查员", value: "梅州市-梅县区-梅县区烟草分局-稽查队-郑稽查员" },
{ label: "谢稽查员", value: "梅州市-梅县区-梅县区烟草分局-稽查队-谢稽查员" }
]
}
]
}
]
},
{
label: "丰顺县", // 县级
value: "丰顺县",
children: [
{
label: "丰顺县烟草分局",
value: "丰顺县烟草分局",
children: [
{ label: "曾分局长", value: "梅州市-丰顺县-丰顺县烟草分局-曾分局长" },
{
label: "专卖管理所",
value: "丰顺县烟草分局-专卖管理所",
children: [
{ label: "邓所长", value: "梅州市-丰顺县-丰顺县烟草分局-专卖管理所-邓所长" },
{ label: "罗管理员", value: "梅州市-丰顺县-丰顺县烟草分局-专卖管理所-罗管理员" }
]
}
]
}
]
}
]
},
{
label: "揭阳市",
value: "揭阳市",
children: [
{
label: "揭阳市烟草局", // 市级局
value: "揭阳市烟草局",
children: [
{ label: "苏局长", value: "揭阳市-揭阳市烟草局-苏局长" },
{ label: "叶副局长", value: "揭阳市-揭阳市烟草局-叶副局长" },
{
label: "专卖监督管理处",
value: "揭阳市烟草局-专卖监督管理处",
children: [
{ label: "潘处长", value: "揭阳市-揭阳市烟草局-专卖监督管理处-潘处长" },
{ label: "方副处长", value: "揭阳市-揭阳市烟草局-专卖监督管理处-方副处长" }
]
}
]
},
{
label: "榕城区",
value: "榕城区",
children: [
{
label: "榕城区烟草分局",
value: "榕城区烟草分局",
children: [
{ label: "王分局长", value: "揭阳市-榕城区-榕城区烟草分局-王分局长" },
{ label: "李明华", value: "揭阳市-榕城区-榕城区烟草分局-李明华" },
{ label: "张丽萍", value: "揭阳市-榕城区-榕城区烟草分局-张丽萍" },
{
label: "市场检查组",
value: "榕城区烟草分局-市场检查组",
children: [
{ label: "陈组长", value: "揭阳市-榕城区-榕城区烟草分局-市场检查组-陈组长" },
{ label: "林检查员", value: "揭阳市-榕城区-榕城区烟草分局-市场检查组-林检查员" }
]
}
]
},
{
label: "榕城区质监局",
value: "榕城区质监局",
children: [
{ label: "陈国强", value: "揭阳市-榕城区-榕城区质监局-陈国强" },
{ label: "林小芳", value: "揭阳市-榕城区-榕城区质监局-林小芳" }
]
}
]
},
{
label: "揭东区",
value: "揭东区",
children: [
{
label: "揭东区烟草分局",
value: "揭东区烟草分局",
children: [
{ label: "黄建军", value: "揭阳市-揭东区-揭东区烟草分局-黄建军" },
{ label: "吴秀英", value: "揭阳市-揭东区-揭东区烟草分局-吴秀英" },
{ label: "刘志华", value: "揭阳市-揭东区-揭东区烟草分局-刘志华" }
]
}
]
},
{
label: "惠来县", // 县级
value: "惠来县",
children: [
{
label: "惠来县烟草分局",
value: "惠来县烟草分局",
children: [
{ label: "杨分局长", value: "揭阳市-惠来县-惠来县烟草分局-杨分局长" },
{
label: "案件审理室",
value: "惠来县烟草分局-案件审理室",
children: [
{ label: "蔡主任", value: "揭阳市-惠来县-惠来县烟草分局-案件审理室-蔡主任" },
{ label: "郭审理员", value: "揭阳市-惠来县-惠来县烟草分局-案件审理室-郭审理员" }
]
}
]
}
]
}
]
},
{
label: "汕头市",
value: "汕头市",
children: [
{
label: "汕头市烟草局", // 市级局
value: "汕头市烟草局",
children: [
{ label: "何局长", value: "汕头市-汕头市烟草局-何局长" },
{ label: "许副局长", value: "汕头市-汕头市烟草局-许副局长" }
]
},
{
label: "龙湖区",
value: "龙湖区",
children: [
{
label: "龙湖区烟草分局",
value: "龙湖区烟草分局",
children: [
{ label: "许志明", value: "汕头市-龙湖区-龙湖区烟草分局-许志明" },
{ label: "蔡丽娜", value: "汕头市-龙湖区-龙湖区烟草分局-蔡丽娜" },
{ label: "郭建华", value: "汕头市-龙湖区-龙湖区烟草分局-郭建华" },
{ label: "何美霞", value: "汕头市-龙湖区-龙湖区烟草分局-何美霞" }
]
},
{
label: "龙湖区工商局",
value: "龙湖区工商局",
children: [
{ label: "方国庆", value: "汕头市-龙湖区-龙湖区工商局-方国庆" },
{ label: "杨小红", value: "汕头市-龙湖区-龙湖区工商局-杨小红" }
]
}
]
},
{
label: "金平区",
value: "金平区",
children: [
{
label: "金平区烟草分局",
value: "金平区烟草分局",
children: [
{ label: "邓志强", value: "汕头市-金平区-金平区烟草分局-邓志强" },
{ label: "罗美玲", value: "汕头市-金平区-金平区烟草分局-罗美玲" }
]
},
{
label: "金平区市场监管局",
value: "金平区市场监管局",
children: [
{ label: "苏建国", value: "汕头市-金平区-金平区市场监管局-苏建国" },
{ label: "叶丽华", value: "汕头市-金平区-金平区市场监管局-叶丽华" },
{ label: "潘志明", value: "汕头市-金平区-金平区市场监管局-潘志明" }
]
}
]
},
{
label: "南澳县", // 县级
value: "南澳县",
children: [
{
label: "南澳县烟草分局",
value: "南澳县烟草分局",
children: [
{ label: "陈分局长", value: "汕头市-南澳县-南澳县烟草分局-陈分局长" },
{ label: "林管理员", value: "汕头市-南澳县-南澳县烟草分局-林管理员" }
]
}
]
}
]
}
];
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" }}
>
{expanded ? "▼" : "▶"}
)}
{ if (el) el.indeterminate = !allChecked && someChecked; }}
onChange={e => onCheck(node, e.target.checked)}
id={node.value}
/>
{expanded && node.children && (
{node.children.map(child => (
))}
)}
);
};
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.ADMINISTRATIVE_PENALTY);
// 步骤状态
const [currentStep, setCurrentStep] = useState(1);
// 步骤1:任务信息
const [taskInfo, setTaskInfo] = useState({
name: '',
date: '',
type: '市局交叉评查',
});
// 步骤2状态
const [groupChecked, setGroupChecked] = useState([]);
// 上传配置状态 - 设置默认值
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);
// 获取当前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();
return (
{/* 步骤指示器 */
{STEPS.map((step) => (
))}
}
{/* 步骤1:创建任务 */}
{currentStep === 1 && (
)}
{/* 步骤2:创建评查小组 */}
{currentStep === 2 && (
<>
{/* 左侧树状多选 */}
{
setGroupChecked(values);
}}
/>
{/* 右侧已选择成员显示区域 */}
已选择的评查小组成员
{groupChecked.length > 0 ? (
{groupChecked.map((member, index) => {
const parts = member.split('-');
const name = parts[parts.length - 1];
const org = parts.slice(0, -1).join(' - ');
return (
);
})}
) : (
暂未选择评查小组成员
)}
共选择 {groupChecked.length} 名成员
{/* 按钮区域移到卡片内部 */}
>
)}
{/* 步骤3:原有上传区域 */}
{currentStep === 3 && (
{/* 案卷类型选择器 */}
选择案卷类型
{/* 文件上传区域 */}
{/* 文件预览区域 */}
{(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 && (
正在上传文件...
正在上传 {uploadType === 'single' ? singleFiles.length : multipleFiles.length} 个文件,请稍候
)}
)}
);
}