feat(evaluation): 模块1.5(1/2) - 优化评查点分组表单验证和父级分组选择

功能变更:
1. 优化父级分组选择
   - 使用增强的 getRuleGroups API 获取父级分组列表
   - 仅显示一级分组(pid: null)
   - 仅显示已启用的分组(is_enabled: true)
   - 提高分组选择的准确性和安全性

2. 新增编码唯一性异步验证
   - 实时验证分组编码唯一性
   - 防抖处理(500ms)避免频繁API调用
   - 编辑模式下自动排除当前分组自身
   - 显示"验证中..."状态提示用户
   - 验证失败时显示清晰的错误提示

3. 改进用户体验
   - 实时反馈编码是否可用
   - 防止提交重复编码的分组
   - 优雅的错误处理和状态管理

技术实现:
- 使用 useState 管理验证状态
- setTimeout 实现防抖机制
- 异步函数处理唯一性检查
- 类型安全的错误处理

验收标准:
 父级分组列表仅显示一级分组
 父级分组列表仅显示已启用的分组
 编码唯一性实时验证(防抖)
 编辑模式下排除自身
 显示验证状态
 无TypeScript类型错误

符合实施计划:
- 阶段 1.5(1/2):rule-groups.new.tsx 更新 

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-25 12:38:41 +08:00
parent fda49b1541
commit 374e3626cc
+72 -2
View File
@@ -100,7 +100,13 @@ export async function loader({ request }: LoaderFunctionArgs) {
// console.log("获取到的ID参数:", id); // console.log("获取到的ID参数:", id);
// 获取一级分组列表 (用于选择父级分组) // 获取一级分组列表 (用于选择父级分组)
const parentGroupsResponse = await getRuleGroups(frontendJWT); // 🆕 使用增强的 getRuleGroups API,仅获取一级分组且已启用的分组
const parentGroupsResponse = await getRuleGroups({
pid: null, // 仅获取一级分组(pid为null表示顶级分组)
is_enabled: true, // 仅获取已启用的分组
pageSize: 100, // 获取足够多的分组
token: frontendJWT
});
if (parentGroupsResponse.error) { if (parentGroupsResponse.error) {
console.error("获取父分组列表失败:", parentGroupsResponse.error); console.error("获取父分组列表失败:", parentGroupsResponse.error);
throw new Error(parentGroupsResponse.error); throw new Error(parentGroupsResponse.error);
@@ -194,7 +200,7 @@ export async function action({ request }: ActionFunctionArgs) {
code: code.trim(), code: code.trim(),
description: description?.trim() || "", description: description?.trim() || "",
is_enabled: status === "active", is_enabled: status === "active",
pid: parentId || undefined // 🆕 NULL/undefined 表示顶级分组 pid: parentId || null // 🆕 NULL 表示顶级分组
}; };
try { try {
@@ -275,6 +281,10 @@ export default function RuleGroupNew() {
general?: string; general?: string;
}>({}); }>({});
// 🆕 编码唯一性验证状态
const [codeValidating, setCodeValidating] = useState(false);
const [codeValidationTimer, setCodeValidationTimer] = useState<NodeJS.Timeout | null>(null);
// 表单引用 // 表单引用
const formRef = useRef<HTMLFormElement>(null); const formRef = useRef<HTMLFormElement>(null);
@@ -310,6 +320,44 @@ export default function RuleGroupNew() {
} }
}, [group]); }, [group]);
// 🆕 异步验证编码唯一性
const validateCodeUnique = async (code: string): Promise<string> => {
if (!code || code.trim() === "") {
return "";
}
try {
setCodeValidating(true);
const response = await getRuleGroups({
code: code.trim(),
pageSize: 10,
token: data.frontendJWT
});
if (response.error) {
console.error("验证编码唯一性失败:", response.error);
return ""; // 验证失败时不显示错误,避免干扰用户
}
if (response.data && response.data.length > 0) {
// 在编辑模式下,排除当前分组自身
const isDuplicate = response.data.some(g =>
g.id !== group?.id && g.code === code.trim()
);
if (isDuplicate) {
return "该编码已被使用,请使用其他编码";
}
}
return "";
} catch (error) {
console.error("验证编码唯一性出错:", error);
return "";
} finally {
setCodeValidating(false);
}
};
// 验证表单字段 // 验证表单字段
const validateField = (field: string, value: string) => { const validateField = (field: string, value: string) => {
switch (field) { switch (field) {
@@ -354,6 +402,27 @@ export default function RuleGroupNew() {
...prev, ...prev,
[name]: error [name]: error
})); }));
// 🆕 编码字段特殊处理:异步验证唯一性(防抖处理)
if (name === 'code' && !error) {
// 清除之前的定时器
if (codeValidationTimer) {
clearTimeout(codeValidationTimer);
}
// 设置新的定时器,500ms后执行验证
const timer = setTimeout(async () => {
const uniqueError = await validateCodeUnique(value);
if (uniqueError) {
setFormErrors(prev => ({
...prev,
code: uniqueError
}));
}
}, 500);
setCodeValidationTimer(timer);
}
}; };
// 处理分组类型更改 // 处理分组类型更改
@@ -551,6 +620,7 @@ export default function RuleGroupNew() {
<div className="form-group"> <div className="form-group">
<label htmlFor="code" className="form-label"> <label htmlFor="code" className="form-label">
<span className="required-mark">*</span> <span className="required-mark">*</span>
{codeValidating && <span className="ml-2 text-sm text-gray-500">...</span>}
</label> </label>
<input <input
type="text" type="text"