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

574 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, Link, 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 { 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 { messageService } from '~/components/ui/MessageModal';
import { toastService } from '~/components/ui/Toast';
import {
getRulesList,
deleteRule,
getRuleTypes,
getRuleGroupsByType,
type RuleType as ApiRuleType,
type RuleGroup
} from '~/api/evaluation_points/rules';
import type { UserRole } from '~/root';
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[]; // 添加评查点类型
userRole: UserRole; // 添加用户角色
};
// 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;
}
interface ActionResponse {
result: boolean;
message: 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);
}
const apiRules = response.data?.rules || [];
const totalCount = response.data?.totalCount || 0;
const rules = apiRules.map((apiRule: ApiRule) => mapApiRuleToModel(apiRule));
// 从sessionStorage获取用户角色
const userRoleFromSession = typeof document !== 'undefined'
? sessionStorage.getItem('userRole') || 'common'
: 'common';
return Response.json({
rules,
totalCount,
currentPage: params.page,
pageSize: params.pageSize,
ruleTypes,
userRole: userRoleFromSession as UserRole
}, {
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 ruleId = formData.get('ruleId');
if (!ruleId) {
return Response.json({ result: false, message: "缺少评查点ID" }, { status: 400 });
}
try {
if (_action === 'delete') {
// 调用API删除评查点
// console.log(`删除评查点 ${ruleId}`);
const deleteResponse = await deleteRule(ruleId as string);
if (deleteResponse.error) {
return Response.json({ result: false, message: deleteResponse.error }, { status: deleteResponse.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 });
}
}
// 规则优先级的描述标签映射
const priorityLabels = {
'high': '高',
'medium': '中',
'low': '低'
};
export default function RulesIndex() {
const loaderData = useLoaderData<typeof loader>();
const { rules, totalCount, currentPage, pageSize, userRole } = loaderData;
const ruleTypes = loaderData.ruleTypes || []; // 添加默认空数组避免undefined
const [searchParams, setSearchParams] = useSearchParams();
const navigate = useNavigate();
const fetcher = useFetcher<ActionResponse>();
// 检查用户是否为开发者角色
const isDeveloper = userRole === 'developer';
// 状态管理
const [ruleGroups, setRuleGroups] = useState<RuleGroup[]>([]);
const [loadingGroups, setLoadingGroups] = useState(false);
// 获取当前的ruleType值
const ruleTypeParam = searchParams.get('ruleType');
// 判断是否禁用规则组选择
const isRuleGroupSelectDisabled = loadingGroups || !ruleTypeParam || ruleGroups.length === 0;
// 使用useEffect监听loaderData.error变化并显示Toast
useEffect(() => {
if(loaderData.error) {
toastService.error(loaderData.error);
}else if(loaderData.ruleTypes.length === 0){
toastService.error("评查点类型数据为空");
}
}, [loaderData.error,loaderData.ruleTypes]);
// 当评查点类型变化时,加载对应的规则组
useEffect(() => {
// 如果选择了"全部"或未选择,则清空规则组
if (!ruleTypeParam || ruleTypeParam === 'all') {
setRuleGroups([]);
return;
}
// 加载当前类型的规则组
const loadRuleGroups = async () => {
setLoadingGroups(true);
try {
const response = await getRuleGroupsByType(ruleTypeParam);
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();
}, [ruleTypeParam]);
// 使用useEffect监听fetcher状态变化并显示Toast
useEffect(() => {
if (fetcher.data && fetcher.state === 'idle') {
if (fetcher.data.result) {
toastService.success(fetcher.data.message);
} else if (!fetcher.data.result) {
if(fetcher.data.message.includes("evaluation_results_evaluation_point_id_fkey")) {
toastService.error('对表evaluation_points进行更新或删除违反了表evaluation results上的外键约束evaluations results_evaluation _point_id_fkey');
} else {
toastService.error(fetcher.data.message);
}
}
}
}, [fetcher.data,fetcher.state]);
// 筛选评查点
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) => {
messageService.show({
title: "确认删除",
message: `确认删除评查点【${rule.name}】吗?`,
type: "warning",
confirmText: "删除",
cancelText: "取消",
onConfirm: () => {
const form = new FormData();
form.append("_action", "delete");
form.append("ruleId", rule.id);
fetcher.submit(form, { method: "post" });
}
});
};
// 复制评查点
const handleCopy = (rule: Rule) => {
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 = () => {
const input = document.querySelector('input[placeholder="输入评查点名称或编码"]');
if (input) {
(input as HTMLInputElement).value = '';
}
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">
{isDeveloper ? (
// 开发者可以看到编辑、复制、删除
<>
<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>
</>
) : (
// 普通用户只能查看
<Link to={`/rules-new?id=${record.id}&mode=view`} className="operation-btn">
<i className="ri-eye-line"></i>
</Link>
)}
</div>
)
}
];
return (
<div className="rules-page">
{/* 页面头部 */}
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-medium"></h2>
{isDeveloper && (
<Button type="primary" icon="ri-add-line" to="/rules-new" className="btn-add-rule">
</Button>
)}
</div>
{/* 筛选区域 */}
<FilterPanel className="px-3 py-3" noActionDivider={true}
actions={
<>
<Button type="default" icon="ri-refresh-line" onClick={handleReset} className="mr-2 hover:!border-gray-300">
</Button>
</>
}
>
<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-[15%]"
/>
<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-[15%]"
/>
<SearchFilter
label="搜索"
placeholder="输入评查点名称或编码"
value={searchParams.get('keyword') || ''}
buttonText="搜索"
onSearch={handleSearch}
className="min-w-[200px] flex-1"
/>
</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>
</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>
);
}