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 { 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"
|
||||||
|
|||||||
Reference in New Issue
Block a user