Files
leaudit-platform-frontend/app/routes/cross-checking._index.tsx
T

560 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from 'react';
import { type MetaFunction, type LoaderFunctionArgs } 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 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,
type CrossCheckingTask,
type TaskListParams,
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;
};
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 [tasksResponse, statsResponse] = await Promise.all([
getCrossCheckingTasks(params),
getCrossCheckingStats()
]);
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 }
}, {
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 }: LoaderFunctionArgs) {
const formData = await request.formData();
const _action = formData.get('_action');
const taskId = formData.get('taskId');
if (!taskId) {
return Response.json({ result: false, message: "缺少任务ID" }, { status: 400 });
}
try {
if (_action === 'delete') {
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 });
}
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 = {
[CrossCheckingTaskType.CITY]: { label: '市级交叉评查', color: 'green' as const },
[CrossCheckingTaskType.COUNTY]: { 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 } = 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 getProgressClass = (progress: number) => {
if (progress === 0) return 'low';
if (progress < 70) return 'medium';
return 'high';
};
// 渲染进度条
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={() => navigate(`/cross-checking/${task.id}`)}
>
<i className="ri-play-line"></i>
</Button>
);
case CrossCheckingTaskStatus.IN_PROGRESS:
return (
<Button
type="default"
size="small"
className="operation-btn secondary"
onClick={() => navigate(`/cross-checking/${task.id}`)}
>
<i className="ri-eye-line"></i>
</Button>
);
case CrossCheckingTaskStatus.COMPLETED:
return (
<Button
type="default"
size="small"
className="operation-btn secondary"
onClick={() => navigate(`/cross-checking/${task.id}/results`)}
>
<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);
};
// 监听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]);
// 定义表格列配置
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%",
render: (value: string, record: CrossCheckingTask) => (
<button
className="task-name text-left w-full"
onClick={() => navigate(`/cross-checking/${record.id}`)}
type="button"
>
{value}
</button>
)
},
{
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];
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];
return (
<Tag color={config.color}>
{config.label}
</Tag>
);
}
},
{
title: "评查地区",
dataIndex: "evaluationRegion" as keyof CrossCheckingTask,
key: "evaluationRegion",
align: "left" as const,
width: "16%"
},
{
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];
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={[
{ value: "", label: "全部类型" },
{ value: CrossCheckingDocType.PENALTY, label: "行政处罚" },
{ value: CrossCheckingDocType.PERMIT, label: "行政许可" }
]}
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>
</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>
);
}