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

499 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";
// 类型定义
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: "创建新的评查点分组,包括分组名称、编码、描述和状态" },
];
};
// 数据加载器
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 parentGroups: ParentGroup[] = [
{ id: "1", name: "合同基本要素检查" },
{ id: "4", name: "销售合同专项检查" },
{ id: "5", name: "行政处罚规范性检查" }
];
// 简化这里,初始时不获取数据,避免可能的错误
let group: RuleGroup | undefined = undefined;
// 如果有ID才尝试获取数据
if (id) {
// 简化的模拟数据
const demoData: Record<string, RuleGroup> = {
"1": {
id: "1",
name: "合同基本要素检查",
code: "contract-base",
description: "检查合同基本要素是否完整规范",
status: "active",
parentId: null,
sortOrder: 0
},
"2": {
id: "2",
name: "必备要素检查",
code: "essential-elements",
description: "检查合同中的必要信息项是否齐全",
status: "active",
parentId: "1",
sortOrder: 1
},
"3": {
id: "3",
name: "合同主体检查",
code: "contract-parties",
description: "检查合同主体信息是否规范",
status: "active",
parentId: "1",
sortOrder: 2
},
"4": {
id: "4",
name: "销售合同专项检查",
code: "contract-sales",
description: "针对销售合同的专项合规检查",
status: "active",
parentId: null,
sortOrder: 3
},
"5": {
id: "5",
name: "行政处罚规范性检查",
code: "punishment",
description: "对行政处罚文书的合规性检查",
status: "inactive",
parentId: null,
sortOrder: 4
}
};
group = demoData[id];
console.log("找到的group数据:", group);
}
// 返回简化的数据结构
return json<LoaderData>({
group,
parentGroups,
isEdit: !!group,
error: undefined // 显式返回error字段,无错误时为undefined
});
} catch (error) {
console.error("loader函数出错:", error);
// 返回一个基本的响应,避免500错误
return json<LoaderData>({
group: undefined,
parentGroups: [],
isEdit: false,
error: "加载数据时出错"
});
}
}
// 表单处理
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 sortOrder = parseInt(formData.get("sortOrder") as string || "0", 10);
// 表单验证
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 json<ActionData>({
errors,
values: Object.fromEntries(formData) as Record<string, string>
});
}
// 构建保存数据
const saveData = {
id,
name: name.trim(),
code: code.trim(),
description: description?.trim() || "",
status,
parentId,
sortOrder
};
try {
// 实际应用中应调用API
console.log("保存分组数据:", saveData);
// const response = await fetch(`${process.env.API_BASE_URL}/api/rule-groups${id ? `/${id}` : ''}`, {
// method: id ? "PUT" : "POST",
// headers: {
// "Content-Type": "application/json",
// },
// body: JSON.stringify(saveData),
// });
//
// if (!response.ok) {
// throw new Error(`保存失败: ${response.status}`);
// }
// 保存成功,重定向到列表页
return redirect("/rule-groups");
} catch (error) {
console.error("保存分组失败:", error);
return json<ActionData>({
success: false,
errors: {
general: "保存分组失败,请稍后重试"
},
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.map((parent) => (
<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" 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>
);
}