import { redirect, type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from "@remix-run/node"; import { useLoaderData, useActionData, useNavigation, Form, useRouteLoaderData } from "@remix-run/react"; import { useEffect, useState, useRef } from "react"; import { Button } from "~/components/ui/Button"; import { Card } from "~/components/ui/Card"; import { toastService } from "~/components/ui/Toast"; 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; } // 样式链接 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 { getUserSession } = await import("~/api/login/auth.server"); const { frontendJWT } = await getUserSession(request); const url = new URL(request.url); const id = url.searchParams.get("id"); // console.log("获取到的ID参数:", id); // 获取一级分组列表 (用于选择父级分组) const parentGroupsResponse = await getRuleGroups(frontendJWT); 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, frontendJWT); 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 { getUserSession } = await import("~/api/login/auth.server"); const { frontendJWT } = await getUserSession(request); // 提取表单数据 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 reviewType = formData.get("reviewType") as string || undefined; // 表单验证 // action是处于服务端的表单提交方法,这里再次验证表单数据也是出于安全考虑,防止客户端验证被绕过从而提交非法数据 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 }); } // 构建保存数据 const saveData: RuleGroupCreateUpdateDto = { name: name.trim(), code: code.trim(), description: description?.trim() || "", is_enabled: status === "active", pid: parentId === null ? "0" : parentId, reviewType: reviewType // 传递 reviewType }; try { // 根据是否有ID决定是创建还是更新 let response; if (id) { response = await updateRuleGroup(id, saveData, frontendJWT); } else { response = await createRuleGroup(saveData, frontendJWT); } // 处理API响应 if (response.error) { console.error("保存分组失败:", response.error); return Response.json({ success: false, errors: { general: response.error }, values: Object.fromEntries(formData) as Record }); } // 保存成功,重定向到列表页 toastService.success("保存成功"); return redirect("/rule-groups"); } catch (error) { console.error("保存分组失败:", error); return Response.json({ success: false, errors: { general: error instanceof Error ? error.message : "保存分组失败,请稍后重试" }, values: Object.fromEntries(formData) as Record }); } } // 页面组件 export default function RuleGroupNew() { // 所有Hooks必须在组件顶部无条件调用 const data = useLoaderData(); const actionData = useActionData(); const navigation = useNavigation(); const isSubmitting = navigation.state === "submitting"; const rootData = useRouteLoaderData("root") as { userRole: string }; const userRole = rootData?.userRole || 'common'; // 判断表单是否为只读模式 const isReadOnly = userRole === 'common'; // 解构数据 const { group, parentGroups, isEdit, error } = data; // 从 sessionStorage 获取 reviewType const [reviewType, setReviewType] = useState(null); // 表单状态管理 - 使用受控组件 const [formValues, setFormValues] = useState<{ groupType: "primary" | "secondary"; name: string; code: string; parentId: string; description: string; status: string; }>({ groupType: group?.parentId ? "secondary" : "primary", name: group?.name || "", code: group?.code || "", parentId: group?.parentId || "", description: group?.description || "", status: group?.status || "active", }); // 表单验证错误状态 const [formErrors, setFormErrors] = useState<{ name?: string; code?: string; parentId?: string; general?: string; }>({}); // 表单引用 const formRef = useRef(null); // 字段是否被触摸过(用于确定何时显示错误) const [touchedFields, setTouchedFields] = useState<{ name: boolean; code: boolean; parentId: boolean; }>({ name: false, code: false, parentId: false }); // 从 actionData 初始化表单错误 useEffect(() => { if (actionData?.errors) { setFormErrors(actionData.errors); } }, [actionData]); // 根据加载的组数据初始化表单 useEffect(() => { if (group) { setFormValues({ groupType: group.parentId ? "secondary" : "primary", name: group.name, code: group.code, parentId: group.parentId || "", description: group.description || "", status: group.status }); } }, [group]); // 在组件挂载时从 sessionStorage 获取 reviewType useEffect(() => { try { if (typeof window !== 'undefined') { const storedReviewType = sessionStorage.getItem('reviewType'); // console.log("从 sessionStorage 获取 reviewType:", storedReviewType); setReviewType(storedReviewType); } } catch (error) { console.error('获取 sessionStorage 中的 reviewType 失败:', error); } }, []); // 验证表单字段 const validateField = (field: string, value: string) => { switch (field) { case 'name': return value.trim() === "" ? "分组名称不能为空" : ""; case 'code': if (value.trim() === "") { return "分组编码不能为空"; } else if (!/^[a-zA-Z0-9-_]+$/.test(value)) { return "分组编码只能包含字母、数字、连字符和下划线"; } return ""; case 'parentId': return formValues.groupType === "secondary" && value.trim() === "" ? "请选择上级分组" : ""; default: return ""; } }; // 处理字段改变 const handleChange = (e: React.ChangeEvent) => { const { name, value } = e.target; setFormValues(prev => ({ ...prev, [name]: value })); // 标记字段为已触摸 if (['name', 'code', 'parentId'].includes(name)) { setTouchedFields(prev => ({ ...prev, [name]: true })); } // 实时验证 const error = validateField(name, value); setFormErrors(prev => ({ ...prev, [name]: error })); }; // 处理分组类型更改 const handleGroupTypeChange = (e: React.ChangeEvent) => { const value = e.target.value as "primary" | "secondary"; setFormValues(prev => ({ ...prev, groupType: value })); // 如果切换为一级分组,清除父分组错误 if (value === "primary") { setFormErrors(prev => ({ ...prev, parentId: "" })); } else if (value === "secondary" && touchedFields.parentId) { // 如果切换为二级分组,且父分组字段已被触摸,重新验证 const error = validateField('parentId', formValues.parentId); setFormErrors(prev => ({ ...prev, parentId: error })); } }; // 处理表单提交前验证 const handleBeforeSubmit = (e: React.FormEvent) => { // 如果是只读模式,阻止提交 if (isReadOnly) { e.preventDefault(); return; } // 标记所有字段为已触摸 setTouchedFields({ name: true, code: true, parentId: true }); // 验证所有字段 const errors = { name: validateField('name', formValues.name), code: validateField('code', formValues.code), parentId: validateField('parentId', formValues.parentId) }; setFormErrors(errors); // 如果有错误,阻止提交 if (errors.name || errors.code || (formValues.groupType === "secondary" && errors.parentId)) { e.preventDefault(); } }; // 如果加载数据时出错,显示错误信息 if (error) { return (

加载出错

{error}

); } return (
{/* 页面头部 */}

{isEdit ? (isReadOnly ? "查看评查点分组" : "编辑评查点分组") : "新增评查点分组"}

创建新的评查点分组,用于组织管理评查点

{!isReadOnly && ( )}
{/* 提示信息 */}

评查点分组用于对评查点进行分类管理,合理的分组结构有助于更高效地组织和查找评查点。

{/* 错误提示 */} {formErrors.general && (
{formErrors.general}
)} {/* 表单 */}
{/* 如果是编辑模式,添加ID */} {group?.id && } {/* 传递 reviewType */} {reviewType && } {/* 基本信息区域 */}

基本信息

{/* 分组类型选择 */}
分组类型 *
一级分组作为顶层分类,二级分组需要选择所属的一级分组
{/* 上级分组选择 */} {formValues.groupType === "secondary" && (
{touchedFields.parentId && formErrors.parentId && (
{formErrors.parentId}
)}
选择此分组所属的上级分组
)} {/* 分组编码和名称 */}
{touchedFields.code && formErrors.code && (
{formErrors.code}
)}
编码只能包含字母、数字、连字符和下划线,且必须唯一
{touchedFields.name && formErrors.name && (
{formErrors.name}
)}
请使用简洁明了的名称,不超过30个字符
{/* 详细配置区域 */}

详细配置

{/* 分组描述 */}
详细描述有助于其他用户了解该分组的用途
{/* 状态 */}
禁用状态的分组及其下的评查点将不会参与评查
{/* 排序 */}
用于设置分组在列表中的显示顺序,默认为0
); }