From 374e3626cce0815e47e7aa5d152c727371cdcbde Mon Sep 17 00:00:00 2001 From: Wenyan Date: Tue, 25 Nov 2025 12:38:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(evaluation):=20=E6=A8=A1=E5=9D=971.5(1/2)?= =?UTF-8?q?=20-=20=E4=BC=98=E5=8C=96=E8=AF=84=E6=9F=A5=E7=82=B9=E5=88=86?= =?UTF-8?q?=E7=BB=84=E8=A1=A8=E5=8D=95=E9=AA=8C=E8=AF=81=E5=92=8C=E7=88=B6?= =?UTF-8?q?=E7=BA=A7=E5=88=86=E7=BB=84=E9=80=89=E6=8B=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 功能变更: 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 --- app/routes/rule-groups.new.tsx | 90 ++++++++++++++++++++++++++++++---- 1 file changed, 80 insertions(+), 10 deletions(-) diff --git a/app/routes/rule-groups.new.tsx b/app/routes/rule-groups.new.tsx index af712d0..ad59e11 100644 --- a/app/routes/rule-groups.new.tsx +++ b/app/routes/rule-groups.new.tsx @@ -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(null); + // 表单引用 const formRef = useRef(null); - + // 字段是否被触摸过(用于确定何时显示错误) const [touchedFields, setTouchedFields] = useState<{ name: boolean; @@ -310,6 +320,44 @@ export default function RuleGroupNew() { } }, [group]); + // 🆕 异步验证编码唯一性 + const validateCodeUnique = async (code: string): Promise => { + 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) => { 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() {