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

570 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, redirect } from "@remix-run/node";
import { useLoaderData, useSearchParams, useSubmit, Link, useNavigate } from "@remix-run/react";
import { Button } from '~/components/ui/Button';
import { Card } from '~/components/ui/Card';
import { Tag } from '~/components/ui/Tag';
import { StatusDot } from '~/components/ui/StatusDot';
import rulesStyles from "~/styles/pages/rules_index.css?url";
import type { Rule, RuleType, RulePriority } from '~/models/rule';
import { RULE_TYPE_COLORS, RULE_PRIORITY_LABELS, RULE_PRIORITY_COLORS } from '~/models/rule';
import type { TagColor } from '~/components/ui/Tag';
import { Table } from '~/components/ui/Table';
import { FilterPanel, FilterSelect, SearchFilter } from '~/components/ui/FilterPanel';
import { Pagination } from '~/components/ui/Pagination';
import {
getRulesList,
deleteRule,
duplicateRule,
getRuleTypes,
getRuleGroupsByType,
type RuleType as ApiRuleType,
type RuleGroup
} from '~/api/evaluation_points/rules';
export const links = () => [
{ rel: "stylesheet", href: rulesStyles }
];
export const meta: MetaFunction = () => {
return [
{ title: "中国烟草AI合同及卷宗审核系统 - 评查点列表" },
{ name: "rules", content: "管理评查点规则,支持根据类型、规则组和状态进行筛选" },
{ name: "keywords", content: "评查点,合同审核,规则管理,中国烟草" }
];
};
// 声明loader返回的数据类型
export type LoaderData = {
rules: Rule[];
totalCount: number;
currentPage: number;
pageSize: number;
totalPages: number;
ruleTypes: ApiRuleType[]; // 添加评查点类型
};
// API返回的数据映射到前端模型
interface ApiRule {
id: string;
code: string;
name: string;
ruleType: string;
groupId: string;
groupName: string;
priority: string;
description: string;
isActive: boolean;
createdAt: string;
updatedAt: string;
}
function mapApiRuleToModel(apiRule: ApiRule): Rule {
return {
id: apiRule.id,
code: apiRule.code,
name: apiRule.name,
ruleType: apiRule.ruleType as RuleType, // 类型转换
ruleGroupId: apiRule.groupId,
groupName: apiRule.groupName,
priority: apiRule.priority as RulePriority, // 类型转换
description: apiRule.description,
checkMethod: 'automatic', // 默认值
prompt: apiRule.description, // 使用描述作为默认prompt
isActive: apiRule.isActive,
createdAt: apiRule.createdAt,
updatedAt: apiRule.updatedAt
};
}
export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
// 从 URL 参数中提取查询条件
const params = {
ruleType: url.searchParams.get("ruleType") || undefined,
groupId: url.searchParams.get("groupId") || undefined,
isActive: url.searchParams.get("isActive") ? url.searchParams.get("isActive") === "true" : undefined,
keyword: url.searchParams.get("keyword") || undefined,
page: parseInt(url.searchParams.get("page") || "1", 10),
pageSize: parseInt(url.searchParams.get("pageSize") || "10", 10)
};
try {
// 获取评查点类型列表
const typeResponse = await getRuleTypes();
if (typeResponse.error) {
console.error('获取评查点类型失败:', typeResponse.error);
}
const ruleTypes = typeResponse.error ? [] : typeResponse.data;
// 使用API调用获取数据
const response = await getRulesList(params);
// API错误处理集中在rules.ts中,这里只需检查是否有错误
if (response.error) {
throw new Error(response.error);
}
if (!response.data) {
throw new Error('API返回数据为空');
}
const apiRules = response.data.rules;
const totalCount = response.data.totalCount;
const rules = apiRules.map((apiRule: ApiRule) => mapApiRuleToModel(apiRule));
// 计算总页数
const totalPages = Math.ceil(totalCount / params.pageSize);
// 验证页码范围
if (params.page < 1 || (totalCount > 0 && params.page > totalPages)) {
const newUrl = new URL(request.url);
newUrl.searchParams.set('page', '1');
return redirect(newUrl.pathname + newUrl.search);
}
return Response.json({
rules,
totalCount,
currentPage: params.page,
pageSize: params.pageSize,
totalPages,
ruleTypes
}, {
headers: {
"Cache-Control": "max-age=60, s-maxage=180"
}
});
} catch (error) {
console.error('加载评查点列表失败:', error);
throw new Response('加载评查点列表失败', { status: 500 });
}
}
export async function action({ request }: LoaderFunctionArgs) {
const formData = await request.formData();
const _action = formData.get('_action');
const ruleId = formData.get('ruleId');
if (!ruleId) {
return Response.json({ success: false, error: "缺少评查点ID" }, { status: 400 });
}
try {
if (_action === 'delete') {
// 调用API删除评查点
console.log(`删除评查点 ${ruleId}`);
try {
const deleteResponse = await deleteRule(ruleId as string);
if (deleteResponse.error) {
throw new Error(deleteResponse.error);
}
// 删除成功,获取当前URL
const url = new URL(request.url);
// 返回重定向响应,以刷新页面数据
return redirect(`${url.pathname}${url.search}`);
} catch (error) {
console.error('删除评查点失败:', error);
return Response.json({
success: false,
error: error instanceof Error ? error.message : "删除失败"
}, { status: 500 });
}
}
if (_action === 'duplicate') {
// 复制评查点
console.log(`复制评查点 ${ruleId}`);
try {
const duplicateResponse = await duplicateRule(ruleId as string);
if (duplicateResponse.error) {
throw new Error(duplicateResponse.error);
}
// 复制成功,获取当前URL
const url = new URL(request.url);
// 返回重定向响应,以刷新页面数据
return redirect(`${url.pathname}${url.search}`);
} catch (error) {
console.error('复制评查点失败:', error);
return Response.json({
success: false,
error: error instanceof Error ? error.message : "复制失败"
}, { status: 500 });
}
}
return Response.json({ success: false, error: "未知操作" }, { status: 400 });
} catch (error) {
console.error('操作评查点失败:', error);
return Response.json({ success: false, error: "操作失败" }, { status: 500 });
}
}
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>
);
}
// 规则优先级的描述标签映射
const priorityLabels = {
'high': '高',
'medium': '中',
'low': '低'
};
export default function RulesIndex() {
const loaderData = useLoaderData<typeof loader>();
const { rules, totalCount, currentPage, pageSize } = loaderData;
const ruleTypes = loaderData.ruleTypes || []; // 添加默认空数组避免undefined
const [searchParams, setSearchParams] = useSearchParams();
const submit = useSubmit();
const navigate = useNavigate();
// 状态管理
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [ruleToDelete, setRuleToDelete] = useState<Rule | null>(null);
const [ruleGroups, setRuleGroups] = useState<RuleGroup[]>([]);
const [loadingGroups, setLoadingGroups] = useState(false);
// 判断是否禁用规则组选择
const isRuleGroupSelectDisabled = loadingGroups || !searchParams.get('ruleType') || ruleGroups.length === 0;
// 当评查点类型变化时,加载对应的规则组
useEffect(() => {
const selectedType = searchParams.get('ruleType');
// 如果选择了"全部"或未选择,则清空规则组
if (!selectedType || selectedType === 'all') {
setRuleGroups([]);
return;
}
// 加载当前类型的规则组
const loadRuleGroups = async () => {
setLoadingGroups(true);
try {
const response = await getRuleGroupsByType(selectedType);
if (response.data) {
setRuleGroups(response.data);
} else if (response.error) {
console.error('加载规则组失败:', response.error);
setRuleGroups([]);
}
} catch (error) {
console.error('加载规则组出错:', error);
setRuleGroups([]);
} finally {
setLoadingGroups(false);
}
};
loadRuleGroups();
}, [searchParams.get('ruleType')]);
const handleFilterChange = (e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>) => {
const { name, value } = e.target;
const newParams = new URLSearchParams(searchParams);
// 如果是规则组选择,但是当前应该被禁用,则不处理
if (name === 'groupId' && isRuleGroupSelectDisabled) {
return;
}
if (value) {
newParams.set(name, value);
// 如果是评查点类型变更,清空规则组选择
if (name === 'ruleType') {
newParams.delete('groupId');
// 如果选择了"全部"或空值,也清空规则组选择
if (value === '' || value === 'all') {
setRuleGroups([]);
}
}
} else {
newParams.delete(name);
// 如果清除评查点类型,也清除规则组
if (name === 'ruleType') {
newParams.delete('groupId');
setRuleGroups([]);
}
}
// 切换筛选条件时,重置到第一页
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 handleDeleteClick = (rule: Rule) => {
console.log("handleDELETEclick",rule)
setRuleToDelete(rule);
setShowDeleteConfirm(true);
};
const confirmDelete = () => {
if (!ruleToDelete) return;
const formData = new FormData();
formData.append('_action', 'delete');
formData.append('ruleId', ruleToDelete.id);
submit(formData, { method: 'post' });
setShowDeleteConfirm(false);
setRuleToDelete(null);
};
const handleCopy = (rule: Rule) => {
// const formData = new FormData();
// formData.append('_action', 'duplicate');
// formData.append('ruleId', rule.id);
// submit(formData, { method: 'post' });
navigate(`/rules-new?id=${rule.id}&mode=copy`);
};
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 = () => {
// setSearchParams(new URLSearchParams());
// };
// 定义表格列配置
const columns = [
{
title: "评查点编码",
dataIndex: "code" as keyof Rule,
key: "code",
align: "left" as const,
width: "20%",
className: "whitespace-normal break-all",
render: (value: string) => (
<div className="whitespace-normal break-all overflow-visible">{value}</div>
)
},
{
title: "评查点名称",
dataIndex: "name" as keyof Rule,
key: "name",
align: "left" as const,
width: "20%"
},
{
title: "评查点类型",
key: "ruleType",
align: "left" as const,
width: "12%",
render: (_: unknown, record: Rule) => {
const typeColor = RULE_TYPE_COLORS[record.ruleType] as TagColor;
return (
record.ruleType ? <Tag color={typeColor}>
{record.ruleType}
</Tag> : null
);
}
},
{
title: "所属规则组",
dataIndex: "groupName" as keyof Rule,
key: "groupName",
align: "left" as const,
width: "10%"
},
{
title: "优先级",
key: "priority",
align: "left" as const,
width: "8%",
render: (_: unknown, record: Rule) => {
const priorityColor = RULE_PRIORITY_COLORS[record.priority] as TagColor;
return (
<Tag color={priorityColor}>
{priorityLabels[record.priority as keyof typeof priorityLabels] || RULE_PRIORITY_LABELS[record.priority]}
</Tag>
);
}
},
{
title: "状态",
key: "isActive",
align: "left" as const,
width: "8%",
render: (_: unknown, record: Rule) => (
<StatusDot status={record.isActive} text={record.isActive ? "启用" : "禁用"} />
)
},
{
title: "创建时间",
dataIndex: "createdAt" as keyof Rule,
key: "createdAt",
align: "left" as const,
width: "10%"
},
{
title: "操作",
key: "operation",
align: "left" as const,
width: "10%",
render: (_: unknown, record: Rule) => (
<div className="operations-cell">
<Link to={`/rules-new?id=${record.id}`} className="operation-btn">
<i className="ri-edit-line"></i>
</Link>
<button className="operation-btn" onClick={() => handleCopy(record)}>
<i className="ri-file-copy-line"></i>
</button>
<button className="operation-btn operation-btn-danger" onClick={() => handleDeleteClick(record)}>
<i className="ri-delete-bin-line"></i>
</button>
</div>
)
}
];
return (
<div className="rules-page">
{/* 页面头部 */}
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-medium"></h2>
<Button type="primary" icon="ri-add-line" to="/rules-new" className="btn-add-rule">
</Button>
</div>
{/* 筛选区域 */}
<FilterPanel>
<FilterSelect
label="评查点类型"
name="ruleType"
value={searchParams.get('ruleType') || ''}
options={[
...ruleTypes.map((type: ApiRuleType) => ({
value: type.id,
label: type.name
}))
]}
onChange={handleFilterChange}
className="mr-3 w-[20%]"
/>
<FilterSelect
label="所属规则组"
name="groupId"
value={searchParams.get('groupId') || ''}
options={[
...(isRuleGroupSelectDisabled ? [{ value: "", label: "请先选择评查点类型" }] : []),
...ruleGroups.map(group => ({
value: group.id,
label: group.name
}))
]}
onChange={handleFilterChange}
className={`mr-3 w-[20%] ${isRuleGroupSelectDisabled ? 'opacity-50' : ''}`}
/>
<FilterSelect
label="状态"
name="isActive"
value={searchParams.get('isActive') || ''}
options={[
{ value: "true", label: "启用" },
{ value: "false", label: "禁用" }
]}
onChange={handleFilterChange}
className="mr-3 w-[20%]"
/>
<SearchFilter
label="搜索"
placeholder="输入评查点名称或编码"
value={searchParams.get('keyword') || ''}
onSearch={handleSearch}
className="w-[30%]"
/>
</FilterPanel>
{/* 评查点列表 - 使用Table组件 */}
<Card className="ant-card">
<Table
columns={columns}
dataSource={rules}
rowKey="id"
emptyText="暂无评查点数据"
className="rules-table"
/>
{/* 分页 */}
{totalCount > 0 && (
<Pagination
currentPage={currentPage}
total={totalCount}
pageSize={pageSize}
onChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
showTotal={true}
showPageSizeChanger={true}
pageSizeOptions={[10, 20, 30, 50]}
/>
)}
</Card>
{/* 删除确认对话框 */}
{showDeleteConfirm && ruleToDelete && (
<div className="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 max-w-md w-full">
<h3 className="text-lg font-medium mb-4"></h3>
<p className="mb-6">&ldquo;{ruleToDelete.name}&rdquo;</p>
<div className="flex justify-end space-x-2">
<Button type="default" onClick={() => setShowDeleteConfirm(false)}></Button>
<Button type="danger" onClick={confirmDelete}></Button>
</div>
</div>
</div>
)}
</div>
);
}