From ac60d647758ddb5067118554c23eb201d67b4128 Mon Sep 17 00:00:00 2001 From: Wenyan Date: Tue, 25 Nov 2025 12:43:01 +0800 Subject: [PATCH] =?UTF-8?q?feat(evaluation):=20=E6=A8=A1=E5=9D=971.5(2/2)?= =?UTF-8?q?=20-=20=E5=A2=9E=E5=BC=BA=E8=AF=84=E6=9F=A5=E7=82=B9=E5=88=86?= =?UTF-8?q?=E7=BB=84=E5=88=97=E8=A1=A8=E9=A1=B5=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 功能变更: 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 --- app/routes/rule-groups._index.tsx | 164 ++++++++++++++++++++++++++++-- 1 file changed, 158 insertions(+), 6 deletions(-) diff --git a/app/routes/rule-groups._index.tsx b/app/routes/rule-groups._index.tsx index 4a86369..3405e23 100644 --- a/app/routes/rule-groups._index.tsx +++ b/app/routes/rule-groups._index.tsx @@ -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() { @@ -27,20 +34,51 @@ 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(); + const loaderData = useLoaderData(); + 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>({}); const [filteredChildrenMap, setFilteredChildrenMap] = useState>({}); const [initialLoading, setInitialLoading] = useState(true); + const [selectedIds, setSelectedIds] = useState([]); // 🆕 批量选择状态 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: ( + 0 && selectedIds.length === groups.length} + onChange={handleSelectAll} + style={{ cursor: 'pointer' }} + /> + ), + key: "selection", + width: "50px", + render: (_: unknown, record: RuleGroup) => ( + handleSelectRow(record.id)} + onClick={(e) => e.stopPropagation()} + style={{ cursor: 'pointer' }} + /> + ) + }] : []), { title: "分组名称", key: "name", @@ -579,6 +703,34 @@ export default function RuleGroupsIndex() { > 收起全部 + {hasEditPermission && selectedIds.length > 0 && ( + <> + + + + + )} {hasEditPermission && (