import { type LoaderFunctionArgs, type MetaFunction } from '@remix-run/node'; import { Link, useLoaderData, useSearchParams } from '@remix-run/react'; import type React from 'react'; import { Button } from '~/components/ui/Button'; import { Card } from '~/components/ui/Card'; import { FilterPanel, FilterSelect, SearchFilter } from '~/components/ui/FilterPanel'; import { Pagination } from '~/components/ui/Pagination'; import { Table } from '~/components/ui/Table'; import { Tag, type TagColor } from '~/components/ui/Tag'; import { loadRuleConfigPackSummaries, type RuleConfigPackSummary } from '~/utils/rules-config-packs.server'; import type { RuleSummary } from '~/utils/rules-yaml-mock.server'; import styles from '~/styles/pages/rules_test.css?url'; export const links = () => [ { rel: 'stylesheet', href: styles } ]; export const meta: MetaFunction = () => [ { title: '规则 YAML 列表 - 智慧法务' } ]; export const handle = { breadcrumb: '评查点列表' }; type RuleRow = RuleSummary & { rowId: string; packId: string; documentType: string; moduleType: string; mainType: string; subtype: string; yamlName: string; yamlStatus: RuleConfigPackSummary['sourceStatus']; isPlaceholder?: boolean; }; type LoaderData = { rows: RuleRow[]; filters: { documentType: string; mainType: string; subtype: string; ruleGroup: string; keyword: string; page: number; pageSize: number; }; totalCount: number; currentPage: number; pageSize: number; options: { documentTypes: string[]; mainTypes: string[]; subtypes: string[]; ruleGroups: string[]; }; }; function unique(values: string[]): string[] { return Array.from(new Set(values.filter(Boolean))); } function resolveDocumentScope(pack: Pick): string { const values = [pack.documentType, pack.mainType, pack.moduleType].join(' '); if (values.includes('合同')) return '合同'; if (values.includes('案卷') || values.includes('卷宗') || values.includes('行政处罚') || values.includes('行政许可')) { return '案卷'; } if (values.includes('公文')) return '内部公文'; return pack.documentType || pack.mainType || pack.moduleType || '未分类'; } function resolveBusinessType(pack: Pick): string { return pack.businessType || pack.mainType || ''; } function riskColor(risk: string): TagColor { if (risk === 'high') return 'red'; if (risk === 'medium') return 'orange'; if (risk === 'low') return 'green'; return 'gray'; } export async function loader({ request }: LoaderFunctionArgs) { const url = new URL(request.url); const requestedMainType = url.searchParams.get('mainType') || url.searchParams.get('ruleTypeName') || ''; const requestedSubtype = url.searchParams.get('subtype') || url.searchParams.get('documentAttributeType') || ''; const requestedRuleGroup = url.searchParams.get('ruleGroup') || url.searchParams.getAll('ruleGroups')[0] || ''; const requestedPage = Number(url.searchParams.get('page') || '1'); const requestedPageSize = Number(url.searchParams.get('pageSize') || '50'); const requestedFilters = { documentType: url.searchParams.get('documentType') || '', mainType: requestedMainType, subtype: requestedSubtype, ruleGroup: requestedRuleGroup, keyword: url.searchParams.get('keyword') || '', page: Number.isFinite(requestedPage) && requestedPage > 0 ? requestedPage : 1, pageSize: [10, 20, 30, 50].includes(requestedPageSize) ? requestedPageSize : 50 }; const packs = await loadRuleConfigPackSummaries(request); const packScopes = packs.map(pack => ({ pack, scope: resolveDocumentScope(pack), })); const documentTypes = unique(packScopes.map(item => item.scope)); const requestedDocumentType = requestedFilters.documentType; const inferredDocumentType = requestedMainType ? packScopes.find(item => resolveBusinessType(item.pack) === requestedMainType)?.scope || '' : ''; const currentDocumentType = documentTypes.includes(requestedDocumentType) ? requestedDocumentType : inferredDocumentType || documentTypes[0] || ''; const scopedDocumentPacks = packScopes .filter(item => item.scope === currentDocumentType) .map(item => item.pack); const scopedFilters = { ...requestedFilters, documentType: currentDocumentType, mainType: scopedDocumentPacks.some(pack => resolveBusinessType(pack) === requestedFilters.mainType) ? requestedFilters.mainType : '', subtype: scopedDocumentPacks.some(pack => (!requestedFilters.mainType || resolveBusinessType(pack) === requestedFilters.mainType) && pack.subtype === requestedFilters.subtype ) ? requestedFilters.subtype : '' }; const scopedByMainTypePacks = scopedDocumentPacks.filter(pack => !scopedFilters.mainType || resolveBusinessType(pack) === scopedFilters.mainType ); const subtypeOptions = unique(scopedByMainTypePacks.map(pack => pack.subtype)); const ruleGroupSourcePacks = scopedFilters.subtype ? scopedByMainTypePacks.filter(pack => pack.subtype === scopedFilters.subtype) : subtypeOptions.length <= 1 ? scopedByMainTypePacks : []; const ruleGroupOptions = unique( ruleGroupSourcePacks.flatMap(pack => pack.rules.map(rule => rule.group)) ).sort((a, b) => a.localeCompare(b, 'zh-CN')); const filters = { ...scopedFilters, ruleGroup: ruleGroupOptions.includes(requestedFilters.ruleGroup) ? requestedFilters.ruleGroup : '' }; const visiblePacks = scopedDocumentPacks.filter(pack => (!filters.mainType || resolveBusinessType(pack) === filters.mainType) && (!filters.subtype || pack.subtype === filters.subtype) ); const filteredRows: RuleRow[] = visiblePacks.flatMap((pack): RuleRow[] => { if (pack.rules.length === 0) { return [{ rowId: `${pack.id}-empty`, packId: pack.id, documentType: pack.documentType, moduleType: pack.moduleType, mainType: resolveBusinessType(pack), subtype: pack.subtype, yamlName: pack.yamlName || '待配置 YAML', yamlStatus: pack.sourceStatus, id: `${pack.id}-empty`, ruleId: '-', name: `${pack.subtype}待配置`, group: '待配置', risk: '-', score: '-', type: '-', checkTypes: [], logic: '', subRules: [], subRuleIds: [], scope: [], dependencies: [], stageCount: 0, appliesIn: [], prompt: '', description: pack.sourceStatus === 'missing' ? '当前规则集已建立,但生效版本正文暂未成功加载,请进入配置页检查并重新保存。' : '当前子类型还没有正式评查点,请进入配置页补充字段、子文档与评查规则。', isPlaceholder: true, }]; } return pack.rules.map(rule => ({ ...rule, rowId: `${pack.id}-${rule.ruleId || rule.id}`, packId: pack.id, documentType: pack.documentType, moduleType: pack.moduleType, mainType: resolveBusinessType(pack), subtype: pack.subtype, yamlName: pack.yamlName, yamlStatus: pack.sourceStatus })); }).filter(row => { if (filters.ruleGroup && row.group !== filters.ruleGroup) { return false; } if (!filters.keyword) return true; return [row.ruleId, row.name, row.group, row.yamlName, row.subtype] .some(value => value.toLowerCase().includes(filters.keyword.toLowerCase())); }); const totalCount = filteredRows.length; const totalPages = Math.max(1, Math.ceil(totalCount / filters.pageSize)); const currentPage = Math.min(filters.page, totalPages); const startIndex = (currentPage - 1) * filters.pageSize; const rows = filteredRows.slice(startIndex, startIndex + filters.pageSize); return Response.json({ rows, filters: { ...filters, page: currentPage }, totalCount, currentPage, pageSize: filters.pageSize, options: { documentTypes, mainTypes: unique(scopedDocumentPacks.map(pack => resolveBusinessType(pack))), subtypes: subtypeOptions, ruleGroups: ruleGroupOptions } } satisfies LoaderData); } export default function RulesTestList() { const { rows, filters, options, totalCount, currentPage, pageSize } = useLoaderData() as LoaderData; const [searchParams, setSearchParams] = useSearchParams(); const updateParams = (updates: Record, resetPage = true) => { const nextParams = new URLSearchParams(searchParams); Object.entries(updates).forEach(([key, value]) => { if (value === undefined || value === '') { nextParams.delete(key); return; } nextParams.set(key, String(value)); }); if (resetPage) { nextParams.set('page', '1'); } setSearchParams(nextParams); }; const handleFilterChange = (event: React.ChangeEvent) => { const { name, value } = event.target; if (name === 'mainType') { updateParams({ mainType: value, ruleTypeName: undefined, subtype: undefined, documentAttributeType: undefined, ruleGroup: undefined, }); return; } if (name === 'subtype') { updateParams({ subtype: value, documentAttributeType: undefined, ruleGroup: undefined }); return; } updateParams({ [name]: value }); }; const handleSearch = (keyword: string) => { updateParams({ keyword }); }; const handleReset = () => { const nextParams = new URLSearchParams(searchParams); ['mainType', 'ruleTypeName', 'ruleGroup', 'subtype', 'documentAttributeType', 'keyword', 'page'].forEach(key => nextParams.delete(key)); nextParams.set('page', '1'); setSearchParams(nextParams); }; const handlePageChange = (page: number) => { updateParams({ page }, false); }; const handlePageSizeChange = (nextPageSize: number) => { updateParams({ pageSize: nextPageSize, page: 1 }, false); }; const columns = [ { title: '规则', key: 'rule', width: '24%', render: (_: unknown, record: RuleRow) => (
{record.name} {record.isPlaceholder ? record.description : record.ruleId}
) }, { title: '子分类', key: 'subtype', width: '12%', align: 'center' as const, render: (_: unknown, record: RuleRow) => (
{record.subtype}
) }, { title: '规则组', dataIndex: 'group' as keyof RuleRow, key: 'group', width: '14%', align: 'center' as const }, { title: '风险', key: 'risk', width: '8%', align: 'center' as const, render: (_: unknown, record: RuleRow) => ( {record.isPlaceholder ? (record.yamlStatus === 'missing' ? '待修复' : '待配置') : record.risk} ) }, { title: '分值', key: 'score', width: '8%', align: 'center' as const, render: (_: unknown, record: RuleRow) => ( {record.isPlaceholder ? '-' : record.score} ) }, { title: '依赖字段', key: 'dependencies', width: '20%', render: (_: unknown, record: RuleRow) => ( {record.isPlaceholder ? '先进入配置页补规则与依赖' : (record.dependencies.length > 0 ? record.dependencies.slice(0, 3).join('、') : '-')} ) }, { title: '操作', key: 'operation', width: '14%', align: 'center' as const, render: (_: unknown, record: RuleRow) => ( {record.isPlaceholder ? '去配置' : '配置'} ) } ]; return (

评查点管理

总评查点数: {totalCount}
重置 } > ({ value: mainType, label: mainType }))} onChange={handleFilterChange} className="mr-3 w-[18%]" placeholder="全部" /> ({ value: subtype, label: subtype }))} onChange={handleFilterChange} className="mr-3 w-[18%]" placeholder={filters.mainType || options.mainTypes.length <= 1 ? '全部' : '请先选择业务类型'} /> ({ value: group, label: group }))} onChange={handleFilterChange} className="mr-3 w-[22%]" disabled={options.ruleGroups.length === 0} placeholder={filters.subtype || options.subtypes.length <= 1 ? '全部' : '请先选择子类型'} /> 暂无规则。当前类型的规则列表流程已保留,可进入配置页查看空 YAML 模板。} /> {totalCount > 0 && ( )} ); }