Files
leaudit-platform-frontend/app/routes/rule-groups.new.tsx
T

487 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.
// app/routes/rule-groups.new.tsx
import { redirect, json, type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from "@remix-run/node";
import { useLoaderData, useActionData, useNavigation, Form } from "@remix-run/react";
import { useEffect, useState } from "react";
import { Button } from "~/components/ui/Button";
import { Card } from "~/components/ui/Card";
import ruleGroupsNewStyles from "~/styles/pages/rule-groups_new.css?url";
import {
getRuleGroups,
getRuleGroup,
createRuleGroup,
updateRuleGroup,
type RuleGroup as ApiRuleGroup,
type RuleGroupCreateUpdateDto
} from "~/api/evaluation_points/rule-groups";
// 类型定义
interface RuleGroup {
id: string;
name: string;
code: string;
description?: string;
status: 'active' | 'inactive';
parentId?: string | null;
sortOrder?: number;
}
interface ParentGroup {
id: string;
name: string;
}
// 定义加载器返回数据类型
export interface LoaderData {
group?: RuleGroup;
parentGroups: ParentGroup[];
isEdit: boolean;
error?: string;
}
// 定义action返回数据类型
export interface ActionData {
success?: boolean;
errors?: {
name?: string;
code?: string;
parentId?: string;
general?: string;
};
values?: Record<string, string>;
}
// 样式链接
export function links() {
return [{ rel: "stylesheet", href: ruleGroupsNewStyles }];
}
// 动态面包屑
export const handle = {
breadcrumb: (data: LoaderData) => {
return data.isEdit ? "编辑分组" : "新增分组";
}
};
// 页面元数据
export const meta: MetaFunction = ({ location }) => {
const isEdit = new URLSearchParams(location.search).has("id");
const title = isEdit ? "编辑评查点分组" : "新建评查点分组";
return [
{ title: `${title} - 中国烟草AI合同及卷宗审核系统` },
{ name: "description", content: "创建新的评查点分组,包括分组名称、编码、描述和状态" },
];
};
// 将API分组转换为前端分组模型
function mapApiToFrontend(apiGroup: ApiRuleGroup): RuleGroup {
return {
id: apiGroup.id,
name: apiGroup.name,
code: apiGroup.code || '',
description: apiGroup.description,
status: apiGroup.is_enabled ? 'active' : 'inactive',
parentId: apiGroup.pid === '0' ? null : apiGroup.pid,
sortOrder: 0 // API中不存在sortOrder字段,使用默认值
};
}
// 数据加载器
export async function loader({ request }: LoaderFunctionArgs) {
console.log("rule-groups.new loader被调用,URL:", request.url);
try {
const url = new URL(request.url);
const id = url.searchParams.get("id");
console.log("获取到的ID参数:", id);
// 获取一级分组列表 (用于选择父级分组)
const parentGroupsResponse = await getRuleGroups();
if (parentGroupsResponse.error) {
console.error("获取父分组列表失败:", parentGroupsResponse.error);
throw new Error(parentGroupsResponse.error);
}
const parentGroups: ParentGroup[] = parentGroupsResponse.data ? parentGroupsResponse.data.map(group => ({
id: group.id,
name: group.name
})) : [];
// 初始化分组数据
let group: RuleGroup | undefined = undefined;
// 如果有ID,获取分组详情
if (id) {
const groupResponse = await getRuleGroup(id);
if (groupResponse.error) {
console.error("获取分组详情失败:", groupResponse.error);
throw new Error(groupResponse.error);
}
if (groupResponse.data) {
group = mapApiToFrontend(groupResponse.data);
}
}
// 返回加载的数据
return Response.json({
group,
parentGroups,
isEdit: !!group,
error: undefined
});
} catch (error) {
console.error("loader函数出错:", error);
// 返回一个基本的响应,避免500错误
return Response.json({
group: undefined,
parentGroups: [],
isEdit: false,
error: error instanceof Error ? error.message : "加载数据时出错"
});
}
}
// 表单处理
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
// 提取表单数据
const id = formData.get("id") as string | null;
const name = formData.get("name") as string;
const code = formData.get("code") as string;
const description = formData.get("description") as string;
const status = formData.get("status") as string || "active";
const groupType = formData.get("groupType") as string;
const parentId = groupType === "secondary" ? formData.get("parentId") as string : null;
// 表单验证
const errors: ActionData["errors"] = {};
if (!name || name.trim() === "") {
errors.name = "分组名称不能为空";
}
if (!code || code.trim() === "") {
errors.code = "分组编码不能为空";
} else if (!/^[a-zA-Z0-9-_]+$/.test(code)) {
errors.code = "分组编码只能包含字母、数字、连字符和下划线";
}
if (groupType === "secondary" && (!parentId || parentId.trim() === "")) {
errors.parentId = "请选择上级分组";
}
if (Object.keys(errors).length > 0) {
return Response.json({
errors,
values: Object.fromEntries(formData) as Record<string, string>
});
}
// 构建保存数据
const saveData: RuleGroupCreateUpdateDto = {
name: name.trim(),
code: code.trim(),
description: description?.trim() || "",
is_enabled: status === "active",
pid: parentId === null ? "0" : parentId
};
try {
// 根据是否有ID决定是创建还是更新
let response;
if (id) {
response = await updateRuleGroup(id, saveData);
} else {
response = await createRuleGroup(saveData);
}
// 处理API响应
if (response.error) {
console.error("保存分组失败:", response.error);
return json<ActionData>({
success: false,
errors: {
general: response.error
},
values: Object.fromEntries(formData) as Record<string, string>
});
}
// 保存成功,重定向到列表页
return redirect("/rule-groups");
} catch (error) {
console.error("保存分组失败:", error);
return json<ActionData>({
success: false,
errors: {
general: error instanceof Error ? error.message : "保存分组失败,请稍后重试"
},
values: Object.fromEntries(formData) as Record<string, string>
});
}
}
// 页面组件
export default function RuleGroupNew() {
// 所有Hooks必须在组件顶部无条件调用
const data = useLoaderData<typeof loader>();
const actionData = useActionData<typeof action>();
const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting";
// 表单状态
const [groupType, setGroupType] = useState<"primary" | "secondary">("primary");
const [showParentSelect, setShowParentSelect] = useState(false);
// 解构数据
const { group, parentGroups, isEdit, error } = data;
// 初始化表单状态
useEffect(() => {
if (group) {
if (group.parentId) {
setGroupType("secondary");
setShowParentSelect(true);
} else {
setGroupType("primary");
setShowParentSelect(false);
}
}
}, [group]);
// 处理分组类型变更
const handleGroupTypeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value as "primary" | "secondary";
setGroupType(value);
setShowParentSelect(value === "secondary");
};
// 如果加载数据时出错,显示错误信息
if (error) {
return (
<div className="error-container">
<h1></h1>
<p>{error}</p>
<Button type="default" to="/rule-groups">
<i className="ri-arrow-left-line"></i>
</Button>
</div>
);
}
return (
<div className="rule-group-new-page">
{/* 页面头部 */}
<div className="page-header">
<div>
<h1 className="page-title">{isEdit ? "编辑评查点分组" : "新增评查点分组"}</h1>
<p className="page-subtitle"></p>
</div>
<div className="header-actions">
<Button
type="default"
to="/rule-groups"
className="mr-3"
>
<i className="ri-arrow-left-line"></i>
</Button>
<Button
type="primary"
form="group-form"
disabled={isSubmitting}
>
<i className="ri-save-line"></i> {isSubmitting ? '保存中...' : '保存分组'}
</Button>
</div>
</div>
<div className="form-container">
{/* 提示信息 */}
<div className="info-message">
<i className="ri-information-line"></i>
<p></p>
</div>
{/* 错误提示 */}
{actionData?.errors?.general && (
<div className="general-error">
<i className="ri-error-warning-line mr-2"></i>
{actionData.errors.general}
</div>
)}
{/* 表单 */}
<Form method="post" id="group-form">
{/* 如果是编辑模式,添加ID */}
{group?.id && <input type="hidden" name="id" value={group.id} />}
{/* 基本信息区域 */}
<Card className="form-section">
<div className="form-section-header">
<i className="ri-file-info-line"></i>
<h3></h3>
</div>
<div className="form-section-body">
{/* 分组类型选择 */}
<div className="form-group">
<legend className="form-label" id="groupTypeLabel">
<span className="required-mark">*</span>
</legend>
<div className="radio-group" role="radiogroup" aria-labelledby="groupTypeLabel">
<label className="radio-item" htmlFor="groupType-primary">
<input
type="radio"
id="groupType-primary"
name="groupType"
className="radio-input"
value="primary"
checked={groupType === "primary"}
onChange={handleGroupTypeChange}
/>
<span></span>
</label>
<label className="radio-item" htmlFor="groupType-secondary">
<input
type="radio"
id="groupType-secondary"
name="groupType"
className="radio-input"
value="secondary"
checked={groupType === "secondary"}
onChange={handleGroupTypeChange}
/>
<span></span>
</label>
</div>
<div className="form-tip"></div>
</div>
{/* 上级分组选择 */}
{showParentSelect && (
<div className="form-group">
<label htmlFor="parentId" className="form-label">
<span className="required-mark">*</span>
</label>
<select
id="parentId"
name="parentId"
className={`form-select ${actionData?.errors?.parentId ? 'error' : ''}`}
defaultValue={group?.parentId || ""}
>
<option value=""></option>
{parentGroups
.filter((parent: ParentGroup) => !group?.id || parent.id !== group.id) // 过滤掉当前编辑的分组
.map((parent: ParentGroup) => (
<option key={parent.id} value={parent.id}>
{parent.name}
</option>
))}
</select>
{actionData?.errors?.parentId && (
<div className="form-error">{actionData.errors.parentId}</div>
)}
<div className="form-tip"></div>
</div>
)}
{/* 分组编码和名称 */}
<div className="form-row">
<div className="form-col">
<div className="form-group">
<label htmlFor="code" className="form-label">
<span className="required-mark">*</span>
</label>
<input
type="text"
id="code"
name="code"
className={`form-input ${actionData?.errors?.code ? 'error' : ''}`}
defaultValue={group?.code || actionData?.values?.code || ""}
placeholder="请输入分组编码,如contract-base"
/>
{actionData?.errors?.code && (
<div className="form-error">{actionData.errors.code}</div>
)}
<div className="form-tip">线</div>
</div>
</div>
<div className="form-col">
<div className="form-group">
<label htmlFor="name" className="form-label">
<span className="required-mark">*</span>
</label>
<input
type="text"
id="name"
name="name"
className={`form-input ${actionData?.errors?.name ? 'error' : ''}`}
defaultValue={group?.name || actionData?.values?.name || ""}
placeholder="请输入分组名称,如合同基本要素检查"
/>
{actionData?.errors?.name && (
<div className="form-error">{actionData.errors.name}</div>
)}
<div className="form-tip">使30</div>
</div>
</div>
</div>
</div>
</Card>
{/* 详细配置区域 */}
<Card className="form-section">
<div className="form-section-header">
<i className="ri-settings-4-line"></i>
<h3></h3>
</div>
<div className="form-section-body">
{/* 分组描述 */}
<div className="form-group">
<label htmlFor="description" className="form-label"></label>
<textarea
id="description"
name="description"
className="form-textarea"
defaultValue={group?.description || actionData?.values?.description || ""}
placeholder="请输入分组描述,包括适用场景、分组目的等"
></textarea>
<div className="form-tip"></div>
</div>
{/* 状态 */}
<div className="form-group" style={{ maxWidth: "400px" }}>
<label htmlFor="status" className="form-label"></label>
<select
id="status"
name="status"
className="form-select"
defaultValue={group?.status || actionData?.values?.status || "active"}
>
<option value="active"></option>
<option value="inactive"></option>
</select>
<div className="form-tip"></div>
</div>
{/* 排序 */}
<div className="form-group hidden" style={{ maxWidth: "400px" }}>
<label htmlFor="sortOrder" className="form-label"></label>
<input
type="number"
id="sortOrder"
name="sortOrder"
className="form-input"
defaultValue={group?.sortOrder?.toString() || actionData?.values?.sortOrder || "0"}
placeholder="请输入排序值,数字越小排序越靠前"
min="0"
/>
<div className="form-tip">0</div>
</div>
</div>
</Card>
</Form>
</div>
</div>
);
}