feat(evaluation): 完成模块2.6 - 评查点前端组件增强
- rules.list.tsx 新增批量操作功能: * 添加批量选择复选框 * 实现批量启用/禁用评查点 * 实现批量删除评查点 * 添加操作结果提示和部分失败处理 - BasicInfo.tsx 新增异步编码验证: * 实现500ms防抖的实时编码唯一性验证 * 集成 getRulesList API 进行编码查重 * 编辑模式下排除当前评查点 * 添加验证中状态和错误提示UI - 通过TypeScript类型检查,无新增类型错误 - 批量操作支持部分成功场景,详细报告结果 - 改善用户体验,提供实时反馈
This commit is contained in:
@@ -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">
|
||||
|
||||
+155
-10
@@ -15,13 +15,15 @@ import { FilterPanel, FilterSelect, SearchFilter } from '~/components/ui/FilterP
|
||||
import { Pagination } from '~/components/ui/Pagination';
|
||||
import { messageService } from '~/components/ui/MessageModal';
|
||||
import { toastService } from '~/components/ui/Toast';
|
||||
import {
|
||||
getRulesList,
|
||||
deleteRule,
|
||||
getRuleTypes,
|
||||
import {
|
||||
getRulesList,
|
||||
deleteRule,
|
||||
getRuleTypes,
|
||||
getRuleGroupsByType,
|
||||
batchUpdateRuleStatus,
|
||||
batchDeleteRules,
|
||||
type RuleType as ApiRuleType,
|
||||
type RuleGroup
|
||||
type RuleGroup
|
||||
} from '~/api/evaluation_points/rules';
|
||||
import type { UserRole } from '~/root';
|
||||
|
||||
@@ -209,6 +211,9 @@ export default function RulesIndex() {
|
||||
// 添加一个状态来跟踪是否执行了删除操作
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
// 批量选择状态
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||
|
||||
// 使用 ref 跟踪是否正在加载数据,避免重复加载
|
||||
const isLoadingRef = useRef(false);
|
||||
|
||||
@@ -541,6 +546,92 @@ export default function RulesIndex() {
|
||||
navigate(`/rules/new?id=${rule.id}&mode=copy`);
|
||||
};
|
||||
|
||||
// 批量选择处理
|
||||
const handleSelectAll = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedIds(filteredRules.map(rule => rule.id));
|
||||
} else {
|
||||
setSelectedIds([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectRow = (id: string) => {
|
||||
setSelectedIds(prev =>
|
||||
prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]
|
||||
);
|
||||
};
|
||||
|
||||
// 批量启用/禁用
|
||||
const handleBatchEnable = async (isEnabled: boolean) => {
|
||||
if (selectedIds.length === 0) {
|
||||
toastService.warning('请先选择要操作的评查点');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const result = await batchUpdateRuleStatus(selectedIds, isEnabled, loaderData.frontendJWT);
|
||||
|
||||
if (result.success) {
|
||||
toastService.success(`成功${isEnabled ? '启用' : '禁用'} ${result.updated_count} 个评查点`);
|
||||
if (result.failed_ids.length > 0) {
|
||||
toastService.warning(`有 ${result.failed_ids.length} 个评查点操作失败`);
|
||||
}
|
||||
// 清空选择
|
||||
setSelectedIds([]);
|
||||
// 重新加载数据
|
||||
fetchData();
|
||||
} else {
|
||||
toastService.error('批量操作失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量操作失败:', error);
|
||||
toastService.error('批量操作失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = async () => {
|
||||
if (selectedIds.length === 0) {
|
||||
toastService.warning('请先选择要删除的评查点');
|
||||
return;
|
||||
}
|
||||
|
||||
messageService.show({
|
||||
title: "确认批量删除",
|
||||
message: `确定要删除选中的 ${selectedIds.length} 个评查点吗?此操作不可恢复。`,
|
||||
type: "warning",
|
||||
confirmText: "删除",
|
||||
cancelText: "取消",
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const result = await batchDeleteRules(selectedIds, loaderData.frontendJWT);
|
||||
|
||||
if (result.success) {
|
||||
toastService.success(`成功删除 ${result.deleted_count} 个评查点`);
|
||||
if (result.failed_ids.length > 0) {
|
||||
toastService.warning(`有 ${result.failed_ids.length} 个评查点删除失败`);
|
||||
}
|
||||
// 清空选择
|
||||
setSelectedIds([]);
|
||||
// 重新加载数据
|
||||
fetchData();
|
||||
} else {
|
||||
toastService.error('批量删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量删除失败:', error);
|
||||
toastService.error('批量删除失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set('page', page.toString());
|
||||
@@ -573,6 +664,28 @@ export default function RulesIndex() {
|
||||
|
||||
// 定义表格列配置
|
||||
const columns = [
|
||||
// 添加复选框列(仅开发者可见)
|
||||
...(isDeveloper ? [{
|
||||
title: (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedIds.length > 0 && selectedIds.length === filteredRules.length && filteredRules.length > 0}
|
||||
onChange={handleSelectAll}
|
||||
disabled={filteredRules.length === 0}
|
||||
/>
|
||||
),
|
||||
key: "selection",
|
||||
align: "center" as const,
|
||||
width: "50px",
|
||||
render: (_: unknown, record: Rule) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedIds.includes(record.id)}
|
||||
onChange={() => handleSelectRow(record.id)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
)
|
||||
}] : []),
|
||||
{
|
||||
title: "评查点编码",
|
||||
dataIndex: "code" as keyof Rule,
|
||||
@@ -691,11 +804,43 @@ export default function RulesIndex() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isDeveloper && (
|
||||
<Button type="primary" icon="ri-add-line" to="/rules/new" className="btn-add-rule">
|
||||
新增评查点
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 批量操作按钮(仅在有选择时显示) */}
|
||||
{isDeveloper && selectedIds.length > 0 && (
|
||||
<>
|
||||
<Button
|
||||
type="default"
|
||||
icon="ri-check-line"
|
||||
onClick={() => handleBatchEnable(true)}
|
||||
className="btn-batch-enable"
|
||||
>
|
||||
批量启用 ({selectedIds.length})
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
icon="ri-close-line"
|
||||
onClick={() => handleBatchEnable(false)}
|
||||
className="btn-batch-disable"
|
||||
>
|
||||
批量禁用 ({selectedIds.length})
|
||||
</Button>
|
||||
<Button
|
||||
type="danger"
|
||||
icon="ri-delete-bin-line"
|
||||
onClick={handleBatchDelete}
|
||||
className="btn-batch-delete"
|
||||
>
|
||||
批量删除 ({selectedIds.length})
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{/* 新增按钮 */}
|
||||
{isDeveloper && (
|
||||
<Button type="primary" icon="ri-add-line" to="/rules/new" className="btn-add-rule">
|
||||
新增评查点
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 筛选区域 */}
|
||||
|
||||
@@ -52,6 +52,7 @@ import { postgrestGet, postgrestPost, postgrestPut } from "~/api/postgrest-clien
|
||||
import { toastService } from '~/components/ui/Toast';
|
||||
import type { UserRole } from '~/root';
|
||||
import { getPromptTemplateOptions } from '~/api/prompts/prompts';
|
||||
import { getRulesList } from '~/api/evaluation_points/rules';
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
@@ -1051,6 +1052,8 @@ export default function RuleNew() {
|
||||
initialData={formData}
|
||||
evaluationPointGroups={evaluationPointGroups}
|
||||
riskOptions={EVALUATION_OPTIONS.riskLevelOptions}
|
||||
frontendJWT={frontendJWT}
|
||||
evaluationPointId={formData.id}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user