import { type MetaFunction } from "@remix-run/node"; import { useLoaderData, Link, useNavigate, useSearchParams } from "@remix-run/react"; import { useState, useEffect } from "react"; import indexStyles from "~/styles/pages/rule-groups_index.css?url"; import { Card } from "~/components/ui/Card"; import { Button } from "~/components/ui/Button"; 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 { getEvaluationPointGroups, getChildGroups, type RuleGroup, deleteEvaluationPointGroup, batchUpdateEvaluationPointGroupStatus, batchDeleteEvaluationPointGroups } from "~/api/evaluation_points/rule-groups"; import { toastService, messageService } from "~/components/ui"; import { usePermission } from "~/hooks/usePermission"; export function links() { return [{ rel: "stylesheet", href: indexStyles }]; } export const meta: MetaFunction = () => { return [ { title: "评查点分组 - 中国烟草AI合同及卷宗审核系统" }, { name: "description", content: "管理评查点分组,包括创建、编辑和删除分组" }, ]; }; export async function loader({ request }: { request: Request }) { try { // 获取用户会话信息 const { getUserSession } = await import("~/api/login/auth.server"); const { frontendJWT } = await getUserSession(request); // 🆕 解析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'); // 🆕 调用 FastAPI v3 的 getEvaluationPointGroups API const response = await getEvaluationPointGroups({ name, code, is_enabled: is_enabled ? is_enabled === 'true' : undefined, pid: null, // 仅获取一级分组 page, pageSize, token: frontendJWT }, frontendJWT); if (response.error) { throw new Error(response.error); } 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: [], totalCount: 0, page: 1, pageSize: 50 }); } } export default function RuleGroupsIndex() { const loaderData = useLoaderData(); const { groups: initialGroups, frontendJWT } = loaderData; const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); const [expandedGroups, setExpandedGroups] = useState([]); const [groups, setGroups] = useState(initialGroups || []); const [loading, setLoading] = useState>({}); const [filteredChildrenMap, setFilteredChildrenMap] = useState>({}); const [initialLoading, setInitialLoading] = useState(true); const [selectedIds, setSelectedIds] = useState([]); // 🆕 批量选择状态 // ✅ 使用权限 Hook const { canCreate, canUpdate, canDelete, canBatch } = usePermission(); const canCreateGroup = canCreate('evaluation_group'); const canUpdateGroup = canUpdate('evaluation_group'); const canDeleteGroup = canDelete('evaluation_group'); const canBatchOperation = canBatch('evaluation_group'); // ✅ 批量操作权限 // 初始加载时自动加载所有子分组 useEffect(() => { const loadAllChildGroups = async () => { if (!initialGroups || initialGroups.length === 0) { setInitialLoading(false); return; } try { // 创建一个加载状态对象,标记所有父分组正在加载 const loadingState: Record = {}; initialGroups.forEach((group: RuleGroup) => { loadingState[group.id] = true; }); setLoading(loadingState); // 并行加载所有父分组的子分组 const promises = initialGroups.map(async (group: RuleGroup) => { try { const response = await getChildGroups(group.id, frontendJWT); if (response.error) { console.error(`加载分组 ${group.id} 的子分组失败:`, response.error); return { parentId: group.id, children: [] }; } return { parentId: group.id, children: response.data }; } catch (error) { console.error(`加载分组 ${group.id} 的子分组出错:`, error); return { parentId: group.id, children: [] }; } }); const results = await Promise.all(promises); // 更新分组数据 setGroups(prev => { const newGroups = [...prev]; results.forEach(({ parentId, children }) => { const parentIndex = newGroups.findIndex(g => g.id === parentId); if (parentIndex !== -1) { newGroups[parentIndex] = { ...newGroups[parentIndex], children }; } }); return newGroups; }); // 展开所有父分组 setExpandedGroups(initialGroups.map((group: RuleGroup) => group.id)); } catch (error) { console.error('自动加载所有子分组失败:', error); } finally { // 清除所有加载状态 setLoading({}); setInitialLoading(false); } }; loadAllChildGroups(); }, [initialGroups]); // 处理展开/收起 const toggleGroup = async (groupId: string) => { if (expandedGroups.includes(groupId)) { // 收起分组 setExpandedGroups(prev => prev.filter(id => id !== groupId)); return; } // 展开分组 setLoading(prev => ({ ...prev, [groupId]: true })); try { const parentGroup = groups.find(g => g.id === groupId); // 如果已经加载过子分组,直接展开 if (parentGroup && parentGroup.children && parentGroup.children.length > 0) { setExpandedGroups(prev => [...prev, groupId]); // 应用现有的过滤条件到已加载的子分组 const nameFilter = searchParams.get('name') || ''; const codeFilter = searchParams.get('code') || ''; const statusFilter = searchParams.get('is_enabled') || ''; if ((nameFilter || codeFilter || statusFilter) && parentGroup.children) { applyFiltersToChildren(groupId, parentGroup.children); } setLoading(prev => ({ ...prev, [groupId]: false })); return; } // 否则加载子分组 const response = await getChildGroups(groupId, frontendJWT); if (response.error) { throw new Error(response.error); } // 更新分组数据 setGroups(prev => prev.map(group => { if (group.id === groupId) { return { ...group, children: response.data }; } return group; })); setExpandedGroups(prev => [...prev, groupId]); // 应用现有的过滤条件到新加载的子分组 const nameFilter = searchParams.get('name') || ''; const codeFilter = searchParams.get('code') || ''; const statusFilter = searchParams.get('is_enabled') || ''; if ((nameFilter || codeFilter || statusFilter) && response.data) { applyFiltersToChildren(groupId, response.data); } } catch (error) { console.error('加载子分组失败:', error); } finally { setLoading(prev => ({ ...prev, [groupId]: false })); } }; // 展开/收起全部 const toggleAll = async (expand: boolean) => { if (expand) { // 展开所有分组 const expandedIds = groups.map(g => g.id); setExpandedGroups(expandedIds); } else { setExpandedGroups([]); } }; // 处理删除分组 const handleDeleteGroup = async (groupId: string) => { // ✅ 检查删除权限 if (!canDeleteGroup) { toastService.warning('您没有删除权限'); return; } messageService.show({ title: "确认删除", message: "确定要删除该分组吗?此操作将同时删除该分组下的所有评查点,且不可恢复。", type: "warning", confirmText: "删除", cancelText: "取消", confirmDelay: 4, onConfirm: async () => { try { const result = await deleteEvaluationPointGroup(groupId, frontendJWT); if (result.success) { // 从本地状态中移除被删除的分组 setGroups(prev => { // 如果是一级分组,直接过滤掉 const filteredGroups = prev.filter(g => g.id !== groupId); // 如果是二级分组,需要从父分组的 children 中移除 return filteredGroups.map(group => { if (group.children) { return { ...group, children: group.children.filter(child => child.id !== groupId) }; } return group; }); }); // 如果被删除的分组当前是展开状态,从展开列表中移除 setExpandedGroups(prev => prev.filter(id => id !== groupId)); // 显示成功消息 toastService.success('删除成功') } else { toastService.error(`删除失败: ${result.error}`); console.error(`删除失败: ${result.error}`); } } catch (error) { console.error('删除分组失败:', error); toastService.error('删除分组失败,请稍后重试'); } } }); }; // 🆕 批量启用/禁用 const handleBatchEnable = async (enable: boolean) => { // ✅ 检查更新权限 if (!canUpdateGroup) { toastService.warning('您没有更新权限'); return; } if (selectedIds.length === 0) { toastService.warning('请先选择要操作的分组'); return; } try { const result = await batchUpdateEvaluationPointGroupStatus(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 (!canDeleteGroup) { toastService.warning('您没有删除权限'); return; } if (selectedIds.length === 0) { toastService.warning('请先选择要删除的分组'); return; } messageService.show({ title: "确认批量删除", message: `确定要删除选中的 ${selectedIds.length} 个分组吗?此操作不可恢复。`, type: "warning", confirmText: "删除", cancelText: "取消", confirmDelay: 4, onConfirm: async () => { try { const result = await batchDeleteEvaluationPointGroups(selectedIds, frontendJWT); // 检查返回状态 if (!result.success) { toastService.error(result.error || '删除失败'); return; } toastService.success(`成功删除 ${result.deleted_groups || 0} 个分组`); // 刷新页面以重新加载数据 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); if (value) { newParams.set('name', value); } else { newParams.delete('name'); } newParams.set('page', '1'); setSearchParams(newParams); }; // 处理搜索编码 const handleCodeSearch = (value: string) => { const newParams = new URLSearchParams(searchParams); if (value) { newParams.set('code', value); } else { newParams.delete('code'); } newParams.set('page', '1'); setSearchParams(newParams); }; // 处理状态筛选变更 const handleStatusChange = (e: React.ChangeEvent) => { const { value } = e.target; const newParams = new URLSearchParams(searchParams); if (value) { newParams.set('is_enabled', value); } else { newParams.delete('is_enabled'); } newParams.set('page', '1'); setSearchParams(newParams); }; // 处理重置筛选 const handleReset = () => { setSearchParams(new URLSearchParams()); setFilteredChildrenMap({}); // 清空输入框内容(通过DOM操作) const nameInput = document.querySelector('input[placeholder="请输入分组名称"]') as HTMLInputElement; const codeInput = document.querySelector('input[placeholder="请输入分组编码"]') as HTMLInputElement; const statusSelect = document.querySelector('select[name="is_enabled"]') as HTMLSelectElement; if (nameInput) nameInput.value = ''; if (codeInput) codeInput.value = ''; if (statusSelect) statusSelect.value = ''; }; // 应用筛选条件到子分组 const applyFiltersToChildren = (parentId: string, children: RuleGroup[]) => { const nameFilter = searchParams.get('name') || ''; const codeFilter = searchParams.get('code') || ''; const statusFilter = searchParams.get('is_enabled') || ''; if (!nameFilter && !codeFilter && !statusFilter) { setFilteredChildrenMap(prev => ({...prev, [parentId]: []})); return; } const filteredChildren = children.filter(child => { // 筛选名称 if (nameFilter && !child.name.toLowerCase().includes(nameFilter.toLowerCase())) { return false; } // 筛选编码 if (codeFilter && (!child.code || !child.code.toLowerCase().includes(codeFilter.toLowerCase()))) { return false; } // 筛选状态 if (statusFilter) { const isEnabled = statusFilter === 'true'; if (child.is_enabled !== isEnabled) { return false; } } return true; }); setFilteredChildrenMap(prev => ({...prev, [parentId]: filteredChildren})); }; // 当筛选条件变化时,重新计算过滤结果 useEffect(() => { const nameFilter = searchParams.get('name') || ''; const codeFilter = searchParams.get('code') || ''; const statusFilter = searchParams.get('is_enabled') || ''; if (!nameFilter && !codeFilter && !statusFilter) { setFilteredChildrenMap({}); return; } // 检查所有已加载的子分组 groups.forEach(group => { if (group.children && group.children.length > 0) { applyFiltersToChildren(group.id, group.children); } }); // 查找匹配的子分组,自动展开它们的父级 const parentsToExpand: string[] = []; groups.forEach(group => { if (group.children && group.children.length > 0) { const hasMatchingChild = group.children.some(child => { const nameMatch = !nameFilter || child.name.toLowerCase().includes(nameFilter.toLowerCase()); const codeMatch = !codeFilter || (child.code && child.code.toLowerCase().includes(codeFilter.toLowerCase())); const statusMatch = !statusFilter || (statusFilter === 'true' ? child.is_enabled : !child.is_enabled); return nameMatch && codeMatch && statusMatch; }); if (hasMatchingChild && !expandedGroups.includes(group.id)) { parentsToExpand.push(group.id); } } }); if (parentsToExpand.length > 0) { setExpandedGroups(prev => [...prev, ...parentsToExpand]); } }, [searchParams, groups]); // 检查父级分组是否匹配筛选条件 const parentMatchesFilter = (group: RuleGroup): boolean => { const nameFilter = searchParams.get('name') || ''; const codeFilter = searchParams.get('code') || ''; const statusFilter = searchParams.get('is_enabled') || ''; // 筛选名称 if (nameFilter && !group.name.toLowerCase().includes(nameFilter.toLowerCase())) { return false; } // 筛选编码 if (codeFilter && (!group.code || !group.code.toLowerCase().includes(codeFilter.toLowerCase()))) { return false; } // 筛选状态 if (statusFilter) { const isEnabled = statusFilter === 'true'; if (group.is_enabled !== isEnabled) { return false; } } return true; }; // 检查父级分组是否有匹配的子分组 const hasMatchingChildren = (parentId: string): boolean => { const filteredChildren = filteredChildrenMap[parentId]; return filteredChildren && filteredChildren.length > 0; }; // 处理表格数据,包括父子级关系 const processedData = groups.flatMap(group => { const parentMatches = parentMatchesFilter(group); const hasMatched = hasMatchingChildren(group.id); // 如果有筛选条件但父级和子级都不匹配,则不显示 const nameFilter = searchParams.get('name') || ''; const codeFilter = searchParams.get('code') || ''; const statusFilter = searchParams.get('is_enabled') || ''; const hasFilters = nameFilter || codeFilter || statusFilter; if (hasFilters && !parentMatches && !hasMatched) { return []; } // 先添加父级分组 const result: (RuleGroup & { isParent?: boolean, parentId?: string })[] = [ { ...group, isParent: true } ]; // 如果有子级分组并且当前已展开,则添加子级分组 if (group.children && expandedGroups.includes(group.id)) { // 如果有筛选条件并且有过滤后的子分组,则显示过滤后的子分组 if (hasFilters && filteredChildrenMap[group.id]) { filteredChildrenMap[group.id].forEach(child => { result.push({ ...child, parentId: group.id }); }); } // 否则显示所有子分组 else if (!hasFilters) { group.children.forEach(child => { result.push({ ...child, parentId: group.id }); }); } } return result; }); // 计算一级分组的评查点数量(累加其所有二级分组的评查点数量) const calculateTotalRuleCount = (record: RuleGroup & { isParent?: boolean, parentId?: string }) => { // 如果是二级分组,直接返回其评查点数量 if (!record.isParent) { return record.ruleCount || 0; } // 如果是一级分组,累加其所有子分组的评查点数量 let totalCount = 0; if (record.children && record.children.length > 0) { totalCount = record.children.reduce((sum, child) => sum + (child.ruleCount || 0), 0); } return totalCount; }; // 定义表格列配置 const columns = [ // 🆕 复选框列 - 仅在有批量操作权限时显示 ...(canBatchOperation ? [{ 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", width: "35%", render: (_: unknown, record: RuleGroup & { isParent?: boolean, parentId?: string }) => (
{record.isParent && ( toggleGroup(record.id)} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleGroup(record.id); } }} > {loading[record.id] ? ( ) : ( )} )} {record.name} {record.isParent ? '一级分组' : '二级分组'}
) }, { title: "分组编码", key: "code", render: (_: unknown, record: RuleGroup) => ( {record.code || '-'} ) }, { title: "评查点数量", key: "ruleCount", width: "12%", render: (_: unknown, record: RuleGroup & { isParent?: boolean, parentId?: string }) => (
{/*
) }, { title: "状态", key: "is_enabled", render: (_: unknown, record: RuleGroup) => ( ) }, { title: "创建时间", key: "createdAt", width: "15%", render: (_: unknown, record: RuleGroup) => ( {record.createdAt || '-'} ) }, { title: "操作", key: "operation", width: "180px", render: (_: unknown, record: RuleGroup) => (
{canDeleteGroup && ( )}
) } ]; return ( //
{/* 页面头部 */}

评查点分组管理
分组数: {processedData.length}

{/* ✅ 批量启用/禁用按钮:仅当有更新权限且有选中项时显示 */} {canUpdateGroup && selectedIds.length > 0 && ( <> )} {/* ✅ 批量删除按钮:仅当有删除权限且有选中项时显示 */} {canDeleteGroup && selectedIds.length > 0 && ( )} {canCreateGroup && ( )}
{/* 搜索栏 - 使用FilterPanel */} {/* */} } noActionDivider={true} > {/* 数据表格 - 使用Table组件 */} {initialLoading ? (
正在加载分组数据...
) : ( <> record.isParent ? `parent-${record.id}` : `child-${record.id}`} emptyText="暂无分组数据" className="tree-table" /> {/* 分页 - 使用Pagination组件 */} {/* {}} showTotal={true} /> */} )} ); }