88466b7a8b
2. 重新对齐交叉评查的接口。
839 lines
26 KiB
TypeScript
839 lines
26 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
||
import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/node";
|
||
import { useLoaderData, useSearchParams, useNavigate, useFetcher } from "@remix-run/react";
|
||
import { Button } from '~/components/ui/Button';
|
||
import { Card } from '~/components/ui/Card';
|
||
import { Tag } from '~/components/ui/Tag';
|
||
import { DocumentListModal } from '~/components/cross-checking';
|
||
|
||
import crossCheckingStyles from "~/styles/pages/cross-checking_index.css?url";
|
||
import { Table } from '~/components/ui/Table';
|
||
import { FilterPanel, FilterSelect, SearchFilter, DateRangeFilter } from '~/components/ui/FilterPanel';
|
||
import { Pagination } from '~/components/ui/Pagination';
|
||
import { toastService } from '~/components/ui/Toast';
|
||
import {
|
||
getCrossCheckingTasks,
|
||
getCrossCheckingStats,
|
||
deleteCrossCheckingTask,
|
||
getCrossCheckingTaskDetail,
|
||
getCrossCheckingDocumentTypes,
|
||
type CrossCheckingTask,
|
||
type TaskDocument,
|
||
type TaskListParams,
|
||
type DocumentType,
|
||
CrossCheckingTaskStatus,
|
||
CrossCheckingTaskType,
|
||
CrossCheckingDocType
|
||
} from '~/api/cross-checking/cross-files';
|
||
|
||
export const links = () => [
|
||
{ rel: "stylesheet", href: crossCheckingStyles }
|
||
];
|
||
|
||
export const meta: MetaFunction = () => {
|
||
return [
|
||
{ title: "中国烟草AI合同及卷宗审核系统 - 交叉评查" },
|
||
{ name: "cross-checking", content: "交叉评查任务管理,支持根据类型、状态和时间进行筛选" },
|
||
{ name: "keywords", content: "交叉评查,任务管理,合同审核,中国烟草" }
|
||
];
|
||
};
|
||
|
||
// 声明loader返回的数据类型
|
||
export type LoaderData = {
|
||
tasks: CrossCheckingTask[];
|
||
totalCount: number;
|
||
currentPage: number;
|
||
pageSize: number;
|
||
totalPages: number;
|
||
stats: {
|
||
totalTasks: number;
|
||
pendingTasks: number;
|
||
inProgressTasks: number;
|
||
completedTasks: number;
|
||
};
|
||
initialLoad?: boolean;
|
||
frontendJWT?: string; // 新增JWT
|
||
documentTypes: DocumentType[]; // 新增:文档类型列表
|
||
documentTypesError?: string; // 新增:文档类型加载错误
|
||
};
|
||
|
||
export async function loader({ request }: LoaderFunctionArgs) {
|
||
const url = new URL(request.url);
|
||
|
||
// 从 URL 参数中提取查询条件
|
||
const params: TaskListParams = {
|
||
page: parseInt(url.searchParams.get("page") || "1", 10),
|
||
pageSize: parseInt(url.searchParams.get("pageSize") || "10", 10),
|
||
taskType: url.searchParams.get("taskType") || undefined,
|
||
docType: url.searchParams.get("docType") || undefined,
|
||
status: url.searchParams.get("status") || undefined,
|
||
keyword: url.searchParams.get("keyword") || undefined,
|
||
dateFrom: url.searchParams.get("dateFrom") || undefined,
|
||
dateTo: url.searchParams.get("dateTo") || undefined
|
||
};
|
||
|
||
try {
|
||
// 获取用户会话信息
|
||
const { getUserSession } = await import("~/api/login/auth.server");
|
||
const { userInfo, frontendJWT } = await getUserSession(request);
|
||
|
||
// console.log('frontendJWT', frontendJWT);
|
||
|
||
// 获取任务列表、统计数据和文档类型,传递用户信息和JWT
|
||
const [tasksResponse, statsResponse, documentTypesResponse] = await Promise.all([
|
||
getCrossCheckingTasks(params, userInfo, frontendJWT),
|
||
getCrossCheckingStats(userInfo, frontendJWT),
|
||
getCrossCheckingDocumentTypes(frontendJWT)
|
||
]);
|
||
|
||
// console.log('tasksResponse', tasksResponse.data?.tasks);
|
||
|
||
if (!tasksResponse.success) {
|
||
console.error('获取任务列表失败:', tasksResponse.error);
|
||
return Response.json({
|
||
error: tasksResponse.error || '获取任务列表失败',
|
||
status: 500
|
||
}, { status: 500 });
|
||
}
|
||
|
||
if (!statsResponse.success) {
|
||
console.error('获取统计数据失败:', statsResponse.error);
|
||
}
|
||
|
||
return Response.json({
|
||
tasks: tasksResponse.data?.tasks || [],
|
||
totalCount: tasksResponse.data?.totalCount || 0,
|
||
currentPage: tasksResponse.data?.currentPage || params.page,
|
||
pageSize: tasksResponse.data?.pageSize || params.pageSize,
|
||
totalPages: tasksResponse.data?.totalPages || 0,
|
||
stats: statsResponse.data || { totalTasks: 0, pendingTasks: 0, inProgressTasks: 0, completedTasks: 0 },
|
||
frontendJWT, // 新增:返回JWT给客户端
|
||
documentTypes: documentTypesResponse.success ? documentTypesResponse.data || [] : [], // 新增:返回文档类型列表
|
||
documentTypesError: documentTypesResponse.error // 新增:返回文档类型加载错误
|
||
}, {
|
||
headers: {
|
||
"Cache-Control": "max-age=60, s-maxage=180"
|
||
}
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('加载交叉评查任务列表失败:', error);
|
||
return Response.json({
|
||
error: error || '加载任务列表失败',
|
||
status: 500
|
||
}, { status: 500 });
|
||
}
|
||
}
|
||
|
||
export async function action({ request }: ActionFunctionArgs) {
|
||
const formData = await request.formData();
|
||
const _action = formData.get('_action');
|
||
const taskId = formData.get('taskId');
|
||
|
||
if (_action === 'delete' && taskId) {
|
||
try {
|
||
const deleteResponse = await deleteCrossCheckingTask(Number(taskId));
|
||
|
||
if (!deleteResponse.success) {
|
||
return Response.json({
|
||
result: false,
|
||
message: deleteResponse.error || '删除任务失败'
|
||
}, { status: 500 });
|
||
}
|
||
|
||
return Response.json({ result: true, message: "任务删除成功" }, { status: 200 });
|
||
} catch (error) {
|
||
console.error('操作任务失败:', error);
|
||
return Response.json({
|
||
result: false,
|
||
message: error instanceof Error ? error.message : "操作失败"
|
||
}, { status: 500 });
|
||
}
|
||
}
|
||
|
||
if (_action === 'getTaskDetail') {
|
||
try {
|
||
// 获取用户会话信息
|
||
const { getUserSession } = await import("~/api/login/auth.server");
|
||
const { frontendJWT } = await getUserSession(request);
|
||
|
||
const page = parseInt(formData.get('page') as string || '1', 10);
|
||
const pageSize = parseInt(formData.get('pageSize') as string || '10', 10);
|
||
|
||
if (!taskId) {
|
||
return Response.json({
|
||
success: false,
|
||
error: "缺少必要参数"
|
||
}, { status: 400 });
|
||
}
|
||
|
||
const response = await getCrossCheckingTaskDetail(
|
||
Number(taskId),
|
||
page,
|
||
pageSize,
|
||
frontendJWT
|
||
);
|
||
|
||
if (response.error) {
|
||
return Response.json({
|
||
success: false,
|
||
error: response.error
|
||
}, { status: 500 });
|
||
}
|
||
|
||
console.log('用户任务详情返回:', response.data);
|
||
return Response.json({
|
||
success: true,
|
||
data: response.data
|
||
});
|
||
} catch (error) {
|
||
console.error('获取任务详情失败:', error);
|
||
return Response.json({
|
||
success: false,
|
||
error: error instanceof Error ? error.message : "获取任务详情失败"
|
||
}, { status: 500 });
|
||
}
|
||
}
|
||
|
||
return Response.json({ result: false, message: "无效的操作" }, { status: 400 });
|
||
}
|
||
|
||
// 状态标签配置
|
||
const statusConfig = {
|
||
[CrossCheckingTaskStatus.PENDING]: { label: '未开始', color: 'yellow' as const },
|
||
[CrossCheckingTaskStatus.IN_PROGRESS]: { label: '进行中', color: 'blue' as const },
|
||
[CrossCheckingTaskStatus.COMPLETED]: { label: '已完成', color: 'green' as const }
|
||
};
|
||
|
||
// 任务类型标签配置
|
||
const taskTypeConfig: Record<string, { label: string; color: 'green' | 'orange' }> = {
|
||
[CrossCheckingTaskType.CITY]: { label: '市局间交叉评查', color: 'green' as const },
|
||
[CrossCheckingTaskType.DISTRICT]: { label: '区局间交叉评查', color: 'orange' as const }
|
||
};
|
||
|
||
// 案卷类型标签配置
|
||
const docTypeConfig = {
|
||
[CrossCheckingDocType.PENALTY]: { label: '行政处罚', color: 'blue' as const },
|
||
[CrossCheckingDocType.PERMIT]: { label: '行政许可', color: 'purple' as const }
|
||
};
|
||
|
||
export default function CrossCheckingIndex() {
|
||
const loaderData = useLoaderData<typeof loader>();
|
||
const { tasks, totalCount, currentPage, pageSize, stats, frontendJWT, documentTypes, documentTypesError } = loaderData;
|
||
const [searchParams, setSearchParams] = useSearchParams();
|
||
const dateFrom = searchParams.get('dateFrom') || '';
|
||
const dateTo = searchParams.get('dateTo') || '';
|
||
const navigate = useNavigate();
|
||
const fetcher = useFetcher();
|
||
|
||
// 状态管理
|
||
const [isDeleting, setIsDeleting] = useState(false);
|
||
const [hasAutoOpened, setHasAutoOpened] = useState(false); // 标记是否已自动打开模态框
|
||
const [modalState, setModalState] = useState<{
|
||
isOpen: boolean;
|
||
title: string;
|
||
files: TaskDocument[];
|
||
loading: boolean;
|
||
// 分页相关状态
|
||
currentPage: number;
|
||
pageSize: number;
|
||
total: number;
|
||
}>({
|
||
isOpen: false,
|
||
title: '',
|
||
files: [],
|
||
loading: false,
|
||
currentPage: 1,
|
||
pageSize: 10,
|
||
total: 0
|
||
});
|
||
|
||
// 客户端调式日志
|
||
// useEffect(()=>{
|
||
// console.log('[CrossCheckingIndex] loaderData.tasks', loaderData.tasks)
|
||
// },[loaderData])
|
||
|
||
// 获取进度条样式类
|
||
const getProgressClass = (progress: number) => {
|
||
if (progress === 0) return 'low';
|
||
if (progress < 70) return 'medium';
|
||
return 'high';
|
||
};
|
||
|
||
// 处理查看结果 - 打开文档列表模态框
|
||
const handleViewResult = async (taskId: number, taskName: string) => {
|
||
// 存储任务信息用于分页
|
||
setCurrentTaskInfo({ taskId, taskName });
|
||
|
||
// 打开模态框
|
||
setModalState(prev => ({
|
||
...prev,
|
||
isOpen: true,
|
||
currentPage: 1,
|
||
pageSize: 10
|
||
}));
|
||
|
||
// 加载第一页数据
|
||
await loadModalData(taskId, 1, 10);
|
||
};
|
||
|
||
// 关闭模态框
|
||
const handleCloseModal = () => {
|
||
setModalState({
|
||
isOpen: false,
|
||
title: '',
|
||
files: [],
|
||
loading: false,
|
||
currentPage: 1,
|
||
pageSize: 10,
|
||
total: 0
|
||
});
|
||
setCurrentTaskInfo(null);
|
||
};
|
||
|
||
// 处理文档查看 - 导航到评查详情页
|
||
const handleViewFile = (fileId: string) => {
|
||
const params = new URLSearchParams({
|
||
id: fileId,
|
||
tId: currentTaskInfo?.taskId?.toString() || '',
|
||
tName: currentTaskInfo?.taskName || '',
|
||
previousRoute: 'crossChecking'
|
||
});
|
||
navigate(`/cross-checking/result?${params.toString()}`);
|
||
};
|
||
|
||
// 存储当前任务信息用于分页
|
||
const [currentTaskInfo, setCurrentTaskInfo] = useState<{
|
||
taskId: number;
|
||
taskName: string;
|
||
} | null>(null);
|
||
|
||
// 加载分页数据
|
||
const loadModalData = async (taskId: number, page: number = 1, pageSize: number = 10) => {
|
||
try {
|
||
setModalState(prev => ({
|
||
...prev,
|
||
loading: true
|
||
}));
|
||
|
||
// 使用 fetcher 调用 action 来获取任务详情
|
||
const formData = new FormData();
|
||
formData.append('_action', 'getTaskDetail');
|
||
formData.append('taskId', taskId.toString());
|
||
formData.append('page', page.toString());
|
||
formData.append('pageSize', pageSize.toString());
|
||
|
||
fetcher.submit(formData, { method: "POST" });
|
||
|
||
} catch (error) {
|
||
console.error('获取任务文档列表失败:', error);
|
||
toastService.error(`获取任务文档列表失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||
|
||
setModalState(prev => ({
|
||
...prev,
|
||
loading: false
|
||
}));
|
||
}
|
||
};
|
||
|
||
// 处理模态框分页变化
|
||
const handleModalPageChange = (page: number) => {
|
||
if (currentTaskInfo) {
|
||
loadModalData(currentTaskInfo.taskId, page, modalState.pageSize);
|
||
}
|
||
};
|
||
|
||
// 处理模态框每页大小变化
|
||
const handleModalPageSizeChange = (size: number) => {
|
||
if (currentTaskInfo) {
|
||
loadModalData(currentTaskInfo.taskId, 1, size);
|
||
}
|
||
};
|
||
|
||
|
||
|
||
// 渲染进度条
|
||
const renderProgress = (progress: number) => (
|
||
<div className="flex items-center space-x-2">
|
||
<div className="progress-bar w-16">
|
||
<div
|
||
className={`progress-bar-fill ${getProgressClass(progress)}`}
|
||
style={{ width: `${progress}%` }}
|
||
/>
|
||
</div>
|
||
<span className="text-sm text-gray-600 min-w-[3rem]">{progress}%</span>
|
||
</div>
|
||
);
|
||
|
||
// 渲染操作按钮
|
||
const renderOperation = (task: CrossCheckingTask) => {
|
||
switch (task.status) {
|
||
case CrossCheckingTaskStatus.PENDING:
|
||
return (
|
||
<Button
|
||
type="primary"
|
||
size="small"
|
||
className="operation-btn primary"
|
||
onClick={() => handleViewResult(task.id,task.taskName)}
|
||
>
|
||
<i className="ri-play-line"></i>
|
||
去评查
|
||
</Button>
|
||
);
|
||
case CrossCheckingTaskStatus.IN_PROGRESS:
|
||
return (
|
||
<Button
|
||
type="default"
|
||
size="small"
|
||
className="operation-btn secondary"
|
||
onClick={() => handleViewResult(task.id,task.taskName)}
|
||
>
|
||
<i className="ri-eye-line"></i>
|
||
进行中
|
||
</Button>
|
||
);
|
||
case CrossCheckingTaskStatus.COMPLETED:
|
||
return (
|
||
<Button
|
||
type="default"
|
||
size="small"
|
||
className="operation-btn secondary"
|
||
onClick={() => handleViewResult(task.id,task.taskName)}
|
||
>
|
||
<i className="ri-file-text-line"></i>
|
||
查看结果
|
||
</Button>
|
||
);
|
||
default:
|
||
return <span>-</span>;
|
||
}
|
||
};
|
||
|
||
// 处理筛选变化
|
||
const handleFilterChange = (e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>) => {
|
||
const { name, value } = e.target;
|
||
const newParams = new URLSearchParams(searchParams);
|
||
|
||
if (value) {
|
||
newParams.set(name, value);
|
||
} else {
|
||
newParams.delete(name);
|
||
}
|
||
|
||
// 切换筛选条件时,重置到第一页
|
||
newParams.set('page', '1');
|
||
setSearchParams(newParams);
|
||
};
|
||
|
||
// 处理搜索
|
||
const handleSearch = (keyword: string) => {
|
||
const newParams = new URLSearchParams(searchParams);
|
||
if (keyword) {
|
||
newParams.set('keyword', keyword);
|
||
} else {
|
||
newParams.delete('keyword');
|
||
}
|
||
|
||
newParams.set('page', '1');
|
||
setSearchParams(newParams);
|
||
};
|
||
|
||
// 处理分页变化
|
||
const handlePageChange = (page: number) => {
|
||
const newParams = new URLSearchParams(searchParams);
|
||
newParams.set('page', page.toString());
|
||
setSearchParams(newParams);
|
||
};
|
||
|
||
// 处理每页条数变化
|
||
const handlePageSizeChange = (size: number) => {
|
||
const newParams = new URLSearchParams(searchParams);
|
||
newParams.set('pageSize', size.toString());
|
||
newParams.set('page', '1');
|
||
setSearchParams(newParams);
|
||
};
|
||
|
||
// 处理重置筛选
|
||
const handleReset = () => {
|
||
const input = document.querySelector('input[placeholder="输入任务名称或评查地区"]');
|
||
if (input) {
|
||
(input as HTMLInputElement).value = '';
|
||
}
|
||
const dateFromInput = document.querySelector('input[name="dateFrom"]');
|
||
const dateToInput = document.querySelector('input[name="dateTo"]');
|
||
if(dateFromInput) {
|
||
(dateFromInput as HTMLInputElement).value = '';
|
||
}
|
||
if(dateToInput) {
|
||
(dateToInput as HTMLInputElement).value = '';
|
||
}
|
||
setSearchParams(new URLSearchParams());
|
||
};
|
||
|
||
// 处理时间范围变更
|
||
const handleDateChange = (field: 'dateFrom' | 'dateTo', value: string) => {
|
||
const newParams = new URLSearchParams(searchParams);
|
||
if(value) {
|
||
newParams.set(field, value);
|
||
} else {
|
||
newParams.delete(field);
|
||
}
|
||
newParams.set('page', '1');
|
||
setSearchParams(newParams);
|
||
};
|
||
|
||
|
||
|
||
// 检测URL参数,自动打开模态框
|
||
useEffect(() => {
|
||
const openModal = searchParams.get('openModal');
|
||
const taskId = searchParams.get('taskId');
|
||
const taskName = searchParams.get('taskName');
|
||
|
||
if (openModal === 'true' && taskId && !hasAutoOpened) {
|
||
console.log('[自动打开模态框] taskId:', taskId, 'taskName:', taskName);
|
||
|
||
// 标记已自动打开,防止重复触发
|
||
setHasAutoOpened(true);
|
||
|
||
// 清除URL参数,避免刷新页面时再次打开
|
||
const newParams = new URLSearchParams(searchParams);
|
||
newParams.delete('openModal');
|
||
newParams.delete('taskId');
|
||
newParams.delete('taskName');
|
||
setSearchParams(newParams, { replace: true });
|
||
|
||
// 延迟自动打开模态框,确保状态已更新
|
||
setTimeout(() => {
|
||
handleViewResult(Number(taskId), taskName || '任务详情');
|
||
}, 100);
|
||
}
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [searchParams, hasAutoOpened]);
|
||
|
||
// 监听fetcher状态变化 - 删除操作
|
||
useEffect(() => {
|
||
if (fetcher.data && fetcher.state === 'idle' && isDeleting) {
|
||
setIsDeleting(false);
|
||
|
||
const data = fetcher.data as { result?: boolean; message?: string };
|
||
if (data.result) {
|
||
toastService.success(data.message || '操作成功');
|
||
// 删除成功后刷新页面
|
||
window.location.reload();
|
||
} else {
|
||
toastService.error(data.message || '操作失败');
|
||
}
|
||
}
|
||
}, [fetcher.data, fetcher.state, isDeleting]);
|
||
|
||
// 监听fetcher状态变化 - 获取任务详情
|
||
useEffect(() => {
|
||
if (fetcher.data && fetcher.state === 'idle' && !isDeleting && modalState.loading) {
|
||
const data = fetcher.data as {
|
||
success?: boolean;
|
||
data?: {
|
||
files: TaskDocument[];
|
||
total: number;
|
||
currentPage: number;
|
||
pageSize: number;
|
||
};
|
||
error?: string;
|
||
};
|
||
|
||
if (data.success && data.data) {
|
||
const { files, total, currentPage, pageSize: returnedPageSize } = data.data;
|
||
|
||
setModalState(prev => ({
|
||
...prev,
|
||
loading: false,
|
||
title: `${currentTaskInfo?.taskName || ''} - 文档列表`,
|
||
files: files || [],
|
||
total: total || 0,
|
||
currentPage: currentPage || prev.currentPage,
|
||
pageSize: returnedPageSize || prev.pageSize
|
||
}));
|
||
} else {
|
||
console.error('获取任务文档列表失败:', data.error);
|
||
toastService.error(`获取任务文档列表失败: ${data.error || '未知错误'}`);
|
||
|
||
setModalState(prev => ({
|
||
...prev,
|
||
loading: false
|
||
}));
|
||
}
|
||
}
|
||
}, [fetcher.data, fetcher.state, isDeleting, modalState.loading, currentTaskInfo?.taskId]);
|
||
|
||
// 定义表格列配置
|
||
const columns = [
|
||
{
|
||
title: "序号",
|
||
dataIndex: "sequence" as keyof CrossCheckingTask,
|
||
key: "sequence",
|
||
align: "center" as const,
|
||
width: "2%"
|
||
},
|
||
{
|
||
title: "任务名称",
|
||
dataIndex: "taskName" as keyof CrossCheckingTask,
|
||
key: "taskName",
|
||
align: "left" as const,
|
||
width: "16%"
|
||
},
|
||
{
|
||
title: "评查开始时间",
|
||
dataIndex: "startDate" as keyof CrossCheckingTask,
|
||
key: "startDate",
|
||
align: "center" as const,
|
||
width: "10%"
|
||
},
|
||
{
|
||
title: "案卷类型",
|
||
key: "docType",
|
||
align: "center" as const,
|
||
width: "8%",
|
||
render: (_: unknown, record: CrossCheckingTask) => {
|
||
const config = docTypeConfig[record.docType as keyof typeof docTypeConfig] || { label: record.docType, color: 'gray' as const };
|
||
return (
|
||
<Tag color={config.color}>
|
||
{config.label}
|
||
</Tag>
|
||
);
|
||
}
|
||
},
|
||
{
|
||
title: "任务类型",
|
||
key: "taskType",
|
||
align: "center" as const,
|
||
width: "10%",
|
||
render: (_: unknown, record: CrossCheckingTask) => {
|
||
const config = taskTypeConfig[record.taskType] || { label: record.taskType, color: 'gray' as const };
|
||
return (
|
||
<Tag color={config.color}>
|
||
{config.label}
|
||
</Tag>
|
||
);
|
||
}
|
||
},
|
||
{
|
||
title: "评查地区",
|
||
key: "evaluationRegion",
|
||
align: "left" as const,
|
||
width: "16%",
|
||
render: (_: unknown, record: CrossCheckingTask) => {
|
||
const regions = record.evaluationRegion;
|
||
|
||
// 如果不是数组,直接显示字符串
|
||
if (!Array.isArray(regions)) {
|
||
return <span className="text-sm">{regions || '-'}</span>;
|
||
}
|
||
|
||
// 如果是空数组
|
||
if (regions.length === 0) {
|
||
return <span className="text-sm text-gray-400">-</span>;
|
||
}
|
||
|
||
// 渲染为标签列表
|
||
return (
|
||
<div className="flex flex-wrap gap-1 items-start">
|
||
{regions.map((region, index) => (
|
||
<Tag
|
||
key={`${region}-${index}`}
|
||
color="cyan"
|
||
>
|
||
{region}
|
||
</Tag>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|
||
},
|
||
{
|
||
title: "评查进度",
|
||
key: "progress",
|
||
align: "left" as const,
|
||
width: "12%",
|
||
render: (_: unknown, record: CrossCheckingTask) => renderProgress(record.progress)
|
||
},
|
||
{
|
||
title: "评查状态",
|
||
key: "status",
|
||
align: "center" as const,
|
||
width: "auto",
|
||
render: (_: unknown, record: CrossCheckingTask) => {
|
||
const config = statusConfig[record.status as keyof typeof statusConfig] || { label: record.status, color: 'gray' as const };
|
||
return (
|
||
<Tag color={config.color}>
|
||
{config.label}
|
||
</Tag>
|
||
);
|
||
}
|
||
},
|
||
// {
|
||
// title: "评查分数",
|
||
// key: "score",
|
||
// align: "center" as const,
|
||
// width: "5%",
|
||
// render: (_: unknown, record: CrossCheckingTask) =>
|
||
// record.status === CrossCheckingTaskStatus.COMPLETED ? record.score : '-'
|
||
// },
|
||
{
|
||
title: "操作",
|
||
key: "operation",
|
||
align: "center" as const,
|
||
width: "auto",
|
||
render: (_: unknown, record: CrossCheckingTask) => renderOperation(record)
|
||
}
|
||
];
|
||
|
||
return (
|
||
<div className="cross-checking-page">
|
||
{/* 页面头部 */}
|
||
<div className="page-header">
|
||
<div className="flex items-center">
|
||
<h2 className="page-title">评查任务</h2>
|
||
<div className="page-stats">
|
||
<div className="stat-item">
|
||
<i className="ri-file-list-3-line stat-icon"></i>
|
||
<span className="text-sm text-gray-600">总任务数:</span>
|
||
<span className="stat-value">{stats.totalTasks}</span>
|
||
</div>
|
||
<div className="stat-item">
|
||
<i className="ri-time-line stat-icon"></i>
|
||
<span className="text-sm text-gray-600">待开始:</span>
|
||
<span className="stat-value">{stats.pendingTasks}</span>
|
||
</div>
|
||
<div className="stat-item">
|
||
<i className="ri-play-circle-line stat-icon"></i>
|
||
<span className="text-sm text-gray-600">进行中:</span>
|
||
<span className="stat-value">{stats.inProgressTasks}</span>
|
||
</div>
|
||
<div className="stat-item">
|
||
<i className="ri-checkbox-circle-line stat-icon"></i>
|
||
<span className="text-sm text-gray-600">已完成:</span>
|
||
<span className="stat-value">{stats.completedTasks}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<Button type="primary" icon="ri-add-line" to="/cross-checking/upload">
|
||
创建任务
|
||
</Button>
|
||
</div>
|
||
|
||
{/* 筛选区域 */}
|
||
<FilterPanel className="px-4 py-4" noActionDivider={true}
|
||
actions={
|
||
<>
|
||
<Button type="default" icon="ri-refresh-line" onClick={handleReset} className="mr-2 hover:!border-gray-300">
|
||
重置
|
||
</Button>
|
||
</>
|
||
}
|
||
>
|
||
<FilterSelect
|
||
label="案卷类型"
|
||
name="docType"
|
||
value={searchParams.get('docType') || ''}
|
||
options={
|
||
documentTypesError
|
||
? []
|
||
: documentTypes && documentTypes.length > 0
|
||
? documentTypes.map((docType: DocumentType) => ({
|
||
value: docType.code,
|
||
label: docType.name
|
||
}))
|
||
: []
|
||
}
|
||
onChange={handleFilterChange}
|
||
className="mr-4 w-[15%]"
|
||
/>
|
||
|
||
<FilterSelect
|
||
label="评查状态"
|
||
name="status"
|
||
value={searchParams.get('status') || ''}
|
||
options={[
|
||
{ value: CrossCheckingTaskStatus.PENDING, label: "未开始" },
|
||
{ value: CrossCheckingTaskStatus.IN_PROGRESS, label: "进行中" },
|
||
{ value: CrossCheckingTaskStatus.COMPLETED, label: "已完成" }
|
||
]}
|
||
onChange={handleFilterChange}
|
||
className="mr-4 w-[15%]"
|
||
/>
|
||
|
||
<DateRangeFilter
|
||
label="时间范围"
|
||
startDate={dateFrom}
|
||
endDate={dateTo}
|
||
onStartDateChange={(value) => handleDateChange('dateFrom', value)}
|
||
onEndDateChange={(value) => handleDateChange('dateTo', value)}
|
||
simple={true}
|
||
colorMode="light"
|
||
/>
|
||
|
||
<SearchFilter
|
||
label="搜索"
|
||
placeholder="输入任务名称或评查地区"
|
||
value={searchParams.get('keyword') || ''}
|
||
buttonText="搜索"
|
||
onSearch={handleSearch}
|
||
className="min-w-[200px] flex-1"
|
||
/>
|
||
</FilterPanel>
|
||
|
||
{/* 任务列表 */}
|
||
<Card className="task-table">
|
||
<div>
|
||
<Table
|
||
columns={columns}
|
||
dataSource={tasks}
|
||
rowKey="id"
|
||
className="cross-checking-table"
|
||
/>
|
||
|
||
{/* 分页 */}
|
||
{totalCount > 0 && (
|
||
<Pagination
|
||
currentPage={currentPage}
|
||
total={totalCount}
|
||
pageSize={pageSize}
|
||
onChange={handlePageChange}
|
||
onPageSizeChange={handlePageSizeChange}
|
||
showTotal={true}
|
||
showPageSizeChanger={true}
|
||
pageSizeOptions={[10, 20, 30, 50]}
|
||
/>
|
||
)}
|
||
</div>
|
||
</Card>
|
||
|
||
{/* 文档列表模态框 */}
|
||
<DocumentListModal
|
||
isOpen={modalState.isOpen}
|
||
onClose={handleCloseModal}
|
||
title={modalState.title}
|
||
files={modalState.files}
|
||
onViewFile={handleViewFile}
|
||
loading={modalState.loading}
|
||
currentPage={modalState.currentPage}
|
||
pageSize={modalState.pageSize}
|
||
total={modalState.total}
|
||
onPageChange={handleModalPageChange}
|
||
onPageSizeChange={handleModalPageSizeChange}
|
||
frontendJWT={frontendJWT}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 错误边界
|
||
export function ErrorBoundary() {
|
||
return (
|
||
<div className="error-container p-6">
|
||
<h1 className="text-xl font-bold text-red-500 mb-4">出错了</h1>
|
||
<p className="mb-4">加载交叉评查任务列表时发生错误。请稍后再试,或联系管理员。</p>
|
||
<Button type="primary" to="/">返回首页</Button>
|
||
</div>
|
||
);
|
||
} |