feat(evaluation): 模块1.5(2/2) - 增强评查点分组列表页功能
功能变更: 1. 服务端筛选和分页 - Loader函数使用增强的 getRuleGroups API - 支持名称、编码、状态筛选 - 支持分页参数(page, pageSize) - 仅加载一级分组(pid: null) - 返回总数用于分页展示 2. 批量操作功能 - 添加批量选择状态管理 - 复选框列(全选/单选) - 批量启用按钮 - 批量禁用按钮 - 批量删除按钮 - 显示选中数量提示 - 操作后自动刷新列表 3. 用户体验优化 - 仅对有编辑权限的用户显示批量操作 - 批量按钮仅在有选中项时显示 - 操作成功/失败的 Toast 提示 - 删除前二次确认 技术实现: - useState 管理选中ID列表 - 条件渲染批量操作按钮 - 类型安全的复选框列定义 - 防止事件冒泡(onClick stopPropagation) - URL参数驱动的服务端筛选 安全性: - 权限检查(hasEditPermission) - 批量删除前确认 - 操作失败详细提示 验收标准: ✅ Loader使用服务端筛选和分页 ✅ 表格支持复选框多选 ✅ 批量操作按钮显示/隐藏正确 ✅ 批量启用/禁用功能正常 ✅ 批量删除功能正常 ✅ 无TypeScript类型错误 ✅ 仅有编辑权限的用户可见批量操作 符合实施计划: - 阶段 1.5(2/2):rule-groups._index.tsx 更新 ✅ - 模块 1.5 完成 ✅ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,14 @@ import { StatusDot } from "~/components/ui/StatusDot";
|
||||
import { Table } from "~/components/ui/Table";
|
||||
import { FilterPanel, FilterSelect, SearchFilter } from "~/components/ui/FilterPanel";
|
||||
// import { Pagination } from "~/components/ui/Pagination";
|
||||
import { getRuleGroups, getChildGroups, type RuleGroup, deleteRuleGroup } from "~/api/evaluation_points/rule-groups";
|
||||
import {
|
||||
getRuleGroups,
|
||||
getChildGroups,
|
||||
type RuleGroup,
|
||||
deleteRuleGroup,
|
||||
batchUpdateRuleGroupStatus,
|
||||
batchDeleteRuleGroups
|
||||
} from "~/api/evaluation_points/rule-groups";
|
||||
import { toastService } from "~/components/ui";
|
||||
|
||||
export function links() {
|
||||
@@ -28,19 +35,50 @@ export async function loader({ request }: { request: Request }) {
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
|
||||
const response = await getRuleGroups(frontendJWT);
|
||||
// 🆕 解析URL查询参数(服务端筛选和分页)
|
||||
const url = new URL(request.url);
|
||||
const name = url.searchParams.get('name') || undefined;
|
||||
const code = url.searchParams.get('code') || undefined;
|
||||
const is_enabled = url.searchParams.get('is_enabled');
|
||||
const page = parseInt(url.searchParams.get('page') || '1');
|
||||
const pageSize = parseInt(url.searchParams.get('pageSize') || '50');
|
||||
|
||||
// 🆕 调用增强的 getRuleGroups API
|
||||
const response = await getRuleGroups({
|
||||
name,
|
||||
code,
|
||||
is_enabled: is_enabled ? is_enabled === 'true' : undefined,
|
||||
pid: null, // 仅获取一级分组
|
||||
page,
|
||||
pageSize,
|
||||
token: frontendJWT
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
return Response.json({ groups: response.data, frontendJWT });
|
||||
|
||||
return Response.json({
|
||||
groups: response.data || [],
|
||||
totalCount: ('totalCount' in response) ? (response.totalCount || 0) : 0,
|
||||
page,
|
||||
pageSize,
|
||||
frontendJWT
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('加载评查点分组失败:', error);
|
||||
return Response.json({ groups: [] });
|
||||
return Response.json({
|
||||
groups: [],
|
||||
totalCount: 0,
|
||||
page: 1,
|
||||
pageSize: 50
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default function RuleGroupsIndex() {
|
||||
const { groups: initialGroups, frontendJWT } = useLoaderData<typeof loader>();
|
||||
const loaderData = useLoaderData<typeof loader>();
|
||||
const { groups: initialGroups, totalCount = 0, page = 1, pageSize = 50, frontendJWT } = loaderData;
|
||||
const rootData = useRouteLoaderData("root") as { userRole: string };
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
@@ -49,6 +87,7 @@ export default function RuleGroupsIndex() {
|
||||
const [loading, setLoading] = useState<Record<string, boolean>>({});
|
||||
const [filteredChildrenMap, setFilteredChildrenMap] = useState<Record<string, RuleGroup[]>>({});
|
||||
const [initialLoading, setInitialLoading] = useState<boolean>(true);
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([]); // 🆕 批量选择状态
|
||||
const userRole = rootData?.userRole || 'common';
|
||||
const hasEditPermission = userRole.toLowerCase().includes('provin');
|
||||
|
||||
@@ -229,6 +268,69 @@ export default function RuleGroupsIndex() {
|
||||
}
|
||||
};
|
||||
|
||||
// 🆕 批量启用/禁用
|
||||
const handleBatchEnable = async (enable: boolean) => {
|
||||
if (selectedIds.length === 0) {
|
||||
toastService.warning('请先选择要操作的分组');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await batchUpdateRuleGroupStatus(selectedIds, enable, frontendJWT);
|
||||
if (result.success) {
|
||||
toastService.success(`成功${enable ? '启用' : '禁用'} ${result.updated_count} 个分组`);
|
||||
// 刷新页面以重新加载数据
|
||||
window.location.reload();
|
||||
} else {
|
||||
toastService.error(`批量操作失败:${result.failed_ids.length} 个分组操作失败`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量操作失败:', error);
|
||||
toastService.error('批量操作失败,请稍后重试');
|
||||
}
|
||||
};
|
||||
|
||||
// 🆕 批量删除
|
||||
const handleBatchDelete = async () => {
|
||||
if (selectedIds.length === 0) {
|
||||
toastService.warning('请先选择要删除的分组');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`确定要删除选中的 ${selectedIds.length} 个分组吗?此操作不可恢复。`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await batchDeleteRuleGroups(selectedIds, frontendJWT);
|
||||
toastService.success(`成功删除 ${result.deleted_count} 个分组`);
|
||||
if (result.failed_ids.length > 0) {
|
||||
toastService.warning(`有 ${result.failed_ids.length} 个分组删除失败`);
|
||||
}
|
||||
// 刷新页面以重新加载数据
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
console.error('批量删除失败:', error);
|
||||
toastService.error('批量删除失败,请稍后重试');
|
||||
}
|
||||
};
|
||||
|
||||
// 🆕 处理全选/取消全选
|
||||
const handleSelectAll = () => {
|
||||
if (selectedIds.length === groups.length) {
|
||||
setSelectedIds([]);
|
||||
} else {
|
||||
setSelectedIds(groups.map(g => g.id));
|
||||
}
|
||||
};
|
||||
|
||||
// 🆕 处理单选
|
||||
const handleSelectRow = (id: string) => {
|
||||
setSelectedIds(prev =>
|
||||
prev.includes(id) ? prev.filter(selectedId => selectedId !== id) : [...prev, id]
|
||||
);
|
||||
};
|
||||
|
||||
// 处理搜索名称
|
||||
const handleNameSearch = (value: string) => {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
@@ -450,6 +552,28 @@ export default function RuleGroupsIndex() {
|
||||
|
||||
// 定义表格列配置
|
||||
const columns = [
|
||||
// 🆕 复选框列
|
||||
...(hasEditPermission ? [{
|
||||
title: (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedIds.length > 0 && selectedIds.length === groups.length}
|
||||
onChange={handleSelectAll}
|
||||
style={{ cursor: 'pointer' }}
|
||||
/>
|
||||
),
|
||||
key: "selection",
|
||||
width: "50px",
|
||||
render: (_: unknown, record: RuleGroup) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedIds.includes(record.id)}
|
||||
onChange={() => handleSelectRow(record.id)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{ cursor: 'pointer' }}
|
||||
/>
|
||||
)
|
||||
}] : []),
|
||||
{
|
||||
title: "分组名称",
|
||||
key: "name",
|
||||
@@ -579,6 +703,34 @@ export default function RuleGroupsIndex() {
|
||||
>
|
||||
收起全部
|
||||
</Button>
|
||||
{hasEditPermission && selectedIds.length > 0 && (
|
||||
<>
|
||||
<Button
|
||||
type="default"
|
||||
icon="ri-checkbox-circle-line"
|
||||
onClick={() => handleBatchEnable(true)}
|
||||
className="mr-2"
|
||||
>
|
||||
批量启用 ({selectedIds.length})
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
icon="ri-close-circle-line"
|
||||
onClick={() => handleBatchEnable(false)}
|
||||
className="mr-2"
|
||||
>
|
||||
批量禁用 ({selectedIds.length})
|
||||
</Button>
|
||||
<Button
|
||||
type="danger"
|
||||
icon="ri-delete-bin-line"
|
||||
onClick={handleBatchDelete}
|
||||
className="mr-2"
|
||||
>
|
||||
批量删除 ({selectedIds.length})
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{hasEditPermission && (
|
||||
<Button
|
||||
type="primary"
|
||||
|
||||
Reference in New Issue
Block a user