feat(evaluation): 完成模块2.6 - 评查点前端组件增强

- rules.list.tsx 新增批量操作功能:
  * 添加批量选择复选框
  * 实现批量启用/禁用评查点
  * 实现批量删除评查点
  * 添加操作结果提示和部分失败处理

- BasicInfo.tsx 新增异步编码验证:
  * 实现500ms防抖的实时编码唯一性验证
  * 集成 getRulesList API 进行编码查重
  * 编辑模式下排除当前评查点
  * 添加验证中状态和错误提示UI

- 通过TypeScript类型检查,无新增类型错误
- 批量操作支持部分成功场景,详细报告结果
- 改善用户体验,提供实时反馈
This commit is contained in:
2025-11-25 13:23:36 +08:00
parent eb5a0c8b47
commit 37134ff650
4 changed files with 307 additions and 15 deletions
+83 -5
View File
@@ -1,15 +1,26 @@
import React, { useState, useEffect } from 'react';
import type { EvaluationPoint } from '~/models/evaluation_points';
import type { EvaluationPointGroup } from '~/models/evaluation_point_groups';
import { getRulesList } from '~/api/evaluation_points/rules';
interface BasicInfoProps {
onChange?: (data: Record<string, unknown>) => void;
initialData?: EvaluationPoint;
evaluationPointGroups?: EvaluationPointGroup[];
riskOptions?: Array<{value: string, label: string}>;
frontendJWT?: string;
evaluationPointId?: number | string;
}
// 评查点基本信息组件
export function BasicInfo({ onChange, initialData, evaluationPointGroups = [], riskOptions = [] }: BasicInfoProps) {
export function BasicInfo({
onChange,
initialData,
evaluationPointGroups = [],
riskOptions = [],
frontendJWT,
evaluationPointId
}: BasicInfoProps) {
const [formData, setFormData] = useState<EvaluationPoint>({
risk: 'medium', // 风险等级 默认中风险
is_enabled: true, // 是否启用 默认启用
@@ -21,6 +32,47 @@ export function BasicInfo({ onChange, initialData, evaluationPointGroups = [], r
...(initialData || {}) // 合并初始数据
});
// 编码验证状态
const [codeValidating, setCodeValidating] = useState(false);
const [codeError, setCodeError] = useState('');
const [codeValidationTimer, setCodeValidationTimer] = useState<NodeJS.Timeout | null>(null);
// 异步验证编码唯一性
const validateCodeUnique = async (code: string): Promise<string> => {
if (!code.trim()) {
return ''; // 空值不验证
}
setCodeValidating(true);
setCodeError('');
try {
const response = await getRulesList({
keyword: code.trim(),
pageSize: 10,
token: frontendJWT
});
if (response.data && response.data.rules && response.data.rules.length > 0) {
// 检查是否有完全匹配的编码(排除当前编辑的评查点)
const isDuplicate = response.data.rules.some(rule =>
rule.code === code.trim() && String(rule.id) !== String(evaluationPointId)
);
if (isDuplicate) {
return '该编码已被使用,请使用其他编码';
}
}
return '';
} catch (error) {
console.error('验证编码唯一性失败:', error);
return ''; // 验证失败不阻止用户输入
} finally {
setCodeValidating(false);
}
};
// 找到当前评查点类型对应的code
const getCheckpointTypeCode = () => {
if (!formData.evaluation_point_groups_pid) return "";
@@ -80,11 +132,23 @@ export function BasicInfo({ onChange, initialData, evaluationPointGroups = [], r
const newData = { ...formData };
// 映射id到表单字段名
switch(id) {
case 'rule-name':
case 'rule-name':
newData.name = value;
break;
case 'rule-code':
case 'rule-code':
newData.code = value;
// 清除之前的验证定时器
if (codeValidationTimer) {
clearTimeout(codeValidationTimer);
}
// 清除错误信息
setCodeError('');
// 设置新的验证定时器(500ms后触发验证)
const timer = setTimeout(async () => {
const error = await validateCodeUnique(value);
setCodeError(error);
}, 500);
setCodeValidationTimer(timer);
break;
case 'risk-level':
newData.risk = value;
@@ -197,6 +261,15 @@ export function BasicInfo({ onChange, initialData, evaluationPointGroups = [], r
// }
// }, [filteredRuleGroups, onChange]);
// 清理验证定时器
useEffect(() => {
return () => {
if (codeValidationTimer) {
clearTimeout(codeValidationTimer);
}
};
}, [codeValidationTimer]);
return (
<div className="ant-card">
<div className="ant-card-header">
@@ -221,16 +294,21 @@ export function BasicInfo({ onChange, initialData, evaluationPointGroups = [], r
<div>
<label className="form-label" htmlFor="rule-code">
<span className="required-mark">*</span>
{codeValidating && <span className="ml-2 text-sm text-gray-500">...</span>}
</label>
<input
type="text"
id="rule-code"
className="form-input"
className={`form-input ${codeError ? 'border-red-500' : ''}`}
placeholder="请输入评查点编码"
value={formData.code}
onChange={handleInputChange}
/>
<div className="form-tip"></div>
{codeError ? (
<div className="form-tip text-red-500">{codeError}</div>
) : (
<div className="form-tip"></div>
)}
</div>
<div>
<label className="form-label" htmlFor="risk-level">