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);
|
// 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentGroups: ParentGroup[] = parentGroupsResponse.data ? parentGroupsResponse.data.map(group => ({
|
const parentGroups: ParentGroup[] = parentGroupsResponse.data ? parentGroupsResponse.data.map(group => ({
|
||||||
id: group.id,
|
id: group.id,
|
||||||
name: group.name
|
name: group.name
|
||||||
@@ -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 {
|
||||||
@@ -274,10 +280,14 @@ export default function RuleGroupNew() {
|
|||||||
parentId?: string;
|
parentId?: string;
|
||||||
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);
|
||||||
|
|
||||||
// 字段是否被触摸过(用于确定何时显示错误)
|
// 字段是否被触摸过(用于确定何时显示错误)
|
||||||
const [touchedFields, setTouchedFields] = useState<{
|
const [touchedFields, setTouchedFields] = useState<{
|
||||||
name: boolean;
|
name: boolean;
|
||||||
@@ -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) {
|
||||||
@@ -323,8 +371,8 @@ export default function RuleGroupNew() {
|
|||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
case 'parentId':
|
case 'parentId':
|
||||||
return formValues.groupType === "secondary" && value.trim() === ""
|
return formValues.groupType === "secondary" && value.trim() === ""
|
||||||
? "请选择上级分组"
|
? "请选择上级分组"
|
||||||
: "";
|
: "";
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
@@ -334,12 +382,12 @@ export default function RuleGroupNew() {
|
|||||||
// 处理字段改变
|
// 处理字段改变
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
|
|
||||||
setFormValues(prev => ({
|
setFormValues(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
[name]: value
|
[name]: value
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 标记字段为已触摸
|
// 标记字段为已触摸
|
||||||
if (['name', 'code', 'parentId'].includes(name)) {
|
if (['name', 'code', 'parentId'].includes(name)) {
|
||||||
setTouchedFields(prev => ({
|
setTouchedFields(prev => ({
|
||||||
@@ -347,13 +395,34 @@ export default function RuleGroupNew() {
|
|||||||
[name]: true
|
[name]: true
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 实时验证
|
// 实时验证
|
||||||
const error = validateField(name, value);
|
const error = validateField(name, value);
|
||||||
setFormErrors(prev => ({
|
setFormErrors(prev => ({
|
||||||
...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"
|
||||||
|
|||||||
Reference in New Issue
Block a user