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:
@@ -100,12 +100,18 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
// 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) {
|
||||
console.error("获取父分组列表失败:", parentGroupsResponse.error);
|
||||
throw new Error(parentGroupsResponse.error);
|
||||
}
|
||||
|
||||
|
||||
const parentGroups: ParentGroup[] = parentGroupsResponse.data ? parentGroupsResponse.data.map(group => ({
|
||||
id: group.id,
|
||||
name: group.name
|
||||
@@ -194,7 +200,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
code: code.trim(),
|
||||
description: description?.trim() || "",
|
||||
is_enabled: status === "active",
|
||||
pid: parentId || undefined // 🆕 NULL/undefined 表示顶级分组
|
||||
pid: parentId || null // 🆕 NULL 表示顶级分组
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -274,10 +280,14 @@ export default function RuleGroupNew() {
|
||||
parentId?: string;
|
||||
general?: string;
|
||||
}>({});
|
||||
|
||||
|
||||
// 🆕 编码唯一性验证状态
|
||||
const [codeValidating, setCodeValidating] = useState(false);
|
||||
const [codeValidationTimer, setCodeValidationTimer] = useState<NodeJS.Timeout | null>(null);
|
||||
|
||||
// 表单引用
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
|
||||
|
||||
// 字段是否被触摸过(用于确定何时显示错误)
|
||||
const [touchedFields, setTouchedFields] = useState<{
|
||||
name: boolean;
|
||||
@@ -310,6 +320,44 @@ export default function RuleGroupNew() {
|
||||
}
|
||||
}, [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) => {
|
||||
switch (field) {
|
||||
@@ -323,8 +371,8 @@ export default function RuleGroupNew() {
|
||||
}
|
||||
return "";
|
||||
case 'parentId':
|
||||
return formValues.groupType === "secondary" && value.trim() === ""
|
||||
? "请选择上级分组"
|
||||
return formValues.groupType === "secondary" && value.trim() === ""
|
||||
? "请选择上级分组"
|
||||
: "";
|
||||
default:
|
||||
return "";
|
||||
@@ -334,12 +382,12 @@ export default function RuleGroupNew() {
|
||||
// 处理字段改变
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
||||
const { name, value } = e.target;
|
||||
|
||||
|
||||
setFormValues(prev => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}));
|
||||
|
||||
|
||||
// 标记字段为已触摸
|
||||
if (['name', 'code', 'parentId'].includes(name)) {
|
||||
setTouchedFields(prev => ({
|
||||
@@ -347,13 +395,34 @@ export default function RuleGroupNew() {
|
||||
[name]: true
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
// 实时验证
|
||||
const error = validateField(name, value);
|
||||
setFormErrors(prev => ({
|
||||
...prev,
|
||||
[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">
|
||||
<label htmlFor="code" className="form-label">
|
||||
分组编码 <span className="required-mark">*</span>
|
||||
{codeValidating && <span className="ml-2 text-sm text-gray-500">验证中...</span>}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
|
||||
Reference in New Issue
Block a user