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:
2025-11-25 12:43:01 +08:00
parent 374e3626cc
commit ac60d64775
+158 -6
View File
@@ -8,7 +8,14 @@ import { StatusDot } from "~/components/ui/StatusDot";
import { Table } from "~/components/ui/Table"; import { Table } from "~/components/ui/Table";
import { FilterPanel, FilterSelect, SearchFilter } from "~/components/ui/FilterPanel"; import { FilterPanel, FilterSelect, SearchFilter } from "~/components/ui/FilterPanel";
// import { Pagination } from "~/components/ui/Pagination"; // 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"; import { toastService } from "~/components/ui";
export function links() { export function links() {
@@ -27,20 +34,51 @@ export async function loader({ request }: { request: Request }) {
// 获取用户会话信息 // 获取用户会话信息
const { getUserSession } = await import("~/api/login/auth.server"); const { getUserSession } = await import("~/api/login/auth.server");
const { frontendJWT } = await getUserSession(request); 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) { if (response.error) {
throw new Error(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) { } catch (error) {
console.error('加载评查点分组失败:', error); console.error('加载评查点分组失败:', error);
return Response.json({ groups: [] }); return Response.json({
groups: [],
totalCount: 0,
page: 1,
pageSize: 50
});
} }
} }
export default function RuleGroupsIndex() { 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 rootData = useRouteLoaderData("root") as { userRole: string };
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
@@ -49,6 +87,7 @@ export default function RuleGroupsIndex() {
const [loading, setLoading] = useState<Record<string, boolean>>({}); const [loading, setLoading] = useState<Record<string, boolean>>({});
const [filteredChildrenMap, setFilteredChildrenMap] = useState<Record<string, RuleGroup[]>>({}); const [filteredChildrenMap, setFilteredChildrenMap] = useState<Record<string, RuleGroup[]>>({});
const [initialLoading, setInitialLoading] = useState<boolean>(true); const [initialLoading, setInitialLoading] = useState<boolean>(true);
const [selectedIds, setSelectedIds] = useState<string[]>([]); // 🆕 批量选择状态
const userRole = rootData?.userRole || 'common'; const userRole = rootData?.userRole || 'common';
const hasEditPermission = userRole.toLowerCase().includes('provin'); 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 handleNameSearch = (value: string) => {
const newParams = new URLSearchParams(searchParams); const newParams = new URLSearchParams(searchParams);
@@ -450,6 +552,28 @@ export default function RuleGroupsIndex() {
// 定义表格列配置 // 定义表格列配置
const columns = [ 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: "分组名称", title: "分组名称",
key: "name", key: "name",
@@ -579,6 +703,34 @@ export default function RuleGroupsIndex() {
> >
</Button> </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 && ( {hasEditPermission && (
<Button <Button
type="primary" type="primary"