feat: sync rule management and review ui fixes

This commit is contained in:
wren
2026-05-07 17:27:42 +08:00
parent 87e82d1caa
commit c00e5feff0
13 changed files with 565 additions and 161 deletions
+68 -17
View File
@@ -82,6 +82,8 @@ interface DocumentListScope {
documentTypeIds: number[];
}
const LIST_SCOPE_STORAGE_KEY = 'documents.listScope';
// 审核状态筛选选项
const auditStatusOptions = [
// { value: "", label: "全部" },
@@ -257,6 +259,10 @@ export default function DocumentsIndex() {
sessionStorage.setItem(SEARCH_PARAMS_STORAGE_KEY, params.toString());
}
};
const persistListScope = useCallback((scope: DocumentListScope) => {
if (typeof window === 'undefined') return;
localStorage.setItem(LIST_SCOPE_STORAGE_KEY, JSON.stringify(scope));
}, []);
// 首次进入且 URL 无任何查询参数时,尝试从 sessionStorage 恢复
useEffect(() => {
@@ -311,7 +317,38 @@ export default function DocumentsIndex() {
}
}
return { selectedModuleId, documentTypeIds };
if (selectedModuleId || documentTypeIds.length > 0) {
const scope = { selectedModuleId, documentTypeIds };
persistListScope(scope);
return scope;
}
const storedScope = localStorage.getItem(LIST_SCOPE_STORAGE_KEY);
if (storedScope) {
try {
const parsedScope = JSON.parse(storedScope) as Partial<DocumentListScope>;
return {
selectedModuleId: Number.isFinite(Number(parsedScope.selectedModuleId)) && Number(parsedScope.selectedModuleId) > 0
? Number(parsedScope.selectedModuleId)
: null,
documentTypeIds: Array.isArray(parsedScope.documentTypeIds)
? parsedScope.documentTypeIds.map((item) => Number(item)).filter((item) => Number.isFinite(item) && item > 0)
: [],
};
} catch (error) {
console.error('解析 localStorage 文档列表作用域失败:', error);
}
}
return { selectedModuleId: null, documentTypeIds: [] };
}, [persistListScope]);
const isDeletableFileStatus = useCallback((status?: string | null) => {
return status === 'Processed' || status === 'Failed';
}, []);
const hasHistoryResultStats = useCallback((doc: DocumentVersionUI) => {
return [doc.pass_count, doc.warning_count, doc.error_count, doc.manual_count].some((value) => value !== null && value !== undefined);
}, []);
// 客户端数据请求
@@ -412,6 +449,7 @@ export default function DocumentsIndex() {
try {
const nextScope = readListScopeFromSession();
setListScope(nextScope);
persistListScope(nextScope);
setScopeReady(true);
} catch (error) {
console.error('❌ [useEffect] 初始化文档列表作用域失败:', error);
@@ -421,7 +459,7 @@ export default function DocumentsIndex() {
setScopeReady(true);
loadingBarService.hide();
}
}, [readListScopeFromSession]);
}, [persistListScope, readListScopeFromSession]);
// 监听 URL 参数变化,重新获取数据
useEffect(() => {
@@ -628,6 +666,11 @@ export default function DocumentsIndex() {
// 下载文档
const handleDownload = (path: string) => {
if (!path) {
toastService.warning('当前版本暂无可下载原文件');
return;
}
// 使用 PDF 代理路由获取文件,自动添加 JWT 认证
const downloadUrl = `/api/pdf-proxy?path=${encodeURIComponent(path)}`;
@@ -650,7 +693,7 @@ export default function DocumentsIndex() {
// 删除文档
const handleDelete = (id: string, name: string, fileStatus: string) => {
// 禁止删除处理中的文件
if (fileStatus !== "Processed" && fileStatus !== "Failed") {
if (!isDeletableFileStatus(fileStatus)) {
toastService.warning("文件正在处理中,无法删除");
return;
}
@@ -684,7 +727,7 @@ export default function DocumentsIndex() {
// 检查是否有正在处理中的文件
const hasProcessingFiles = documents.some((doc: DocumentUI) =>
selectedRowKeys.includes(doc.id.toString()) && doc.fileStatus !== 'Processed'
selectedRowKeys.includes(doc.id.toString()) && !isDeletableFileStatus(doc.fileStatus)
);
if (hasProcessingFiles) {
@@ -1149,6 +1192,10 @@ export default function DocumentsIndex() {
// 渲染历史版本行的辅助函数
const renderHistoryRow = (historyDoc: DocumentVersionUI, parentDoc: DocumentUI) => {
const canDownloadHistory = Boolean(historyDoc.path);
const canShowHistoryStats = hasHistoryResultStats(historyDoc);
const canAppendHistoryAssets = parentDoc.type === '1' && historyDoc.fileStatus === 'Processed' && canDownloadHistory;
return (
<tr key={`history-${historyDoc.id}`} className="history-row">
<td className="align-middle px-4 py-3" style={{ width: '50px' }}>
@@ -1194,16 +1241,20 @@ export default function DocumentsIndex() {
})()}
</td>
<td className="px-4 py-3" style={{ width: '15%' }}>
<ResultStats
passCount={historyDoc.pass_count}
warningCount={historyDoc.warning_count}
errorCount={historyDoc.error_count}
manualCount={historyDoc.manual_count}
previousPassCount={historyDoc.previous_pass_count}
previousWarningCount={historyDoc.previous_warning_count}
previousErrorCount={historyDoc.previous_error_count}
previousManualCount={historyDoc.previous_manual_count}
/>
{canShowHistoryStats ? (
<ResultStats
passCount={historyDoc.pass_count}
warningCount={historyDoc.warning_count}
errorCount={historyDoc.error_count}
manualCount={historyDoc.manual_count}
previousPassCount={historyDoc.previous_pass_count}
previousWarningCount={historyDoc.previous_warning_count}
previousErrorCount={historyDoc.previous_error_count}
previousManualCount={historyDoc.previous_manual_count}
/>
) : (
<span className="text-xs text-gray-400">-</span>
)}
</td>
<td className="text-xs text-gray-600 px-4 py-3" style={{ width: '10%' }}>{historyDoc.uploadTime}</td>
<td className="px-4 py-3" style={{ width: '25%' }}>
@@ -1230,7 +1281,7 @@ export default function DocumentsIndex() {
</Link>
)}
{/* 下载按钮 - 需要 document:document:view 权限 */}
{canView && (
{canView && canDownloadHistory && (
<button
type="button"
className="text-xs px-2 py-1 h-7 mr-1 text-gray-500 hover:underline hover:text-gray-700"
@@ -1241,7 +1292,7 @@ export default function DocumentsIndex() {
</button>
)}
{/* 追加附件和上传模板按钮 - 需要 document:document:view 权限 */}
{canView && parentDoc.type === '1' && historyDoc.fileStatus === 'Processed' && (
{canView && canAppendHistoryAssets && (
<>
<button
type="button"
@@ -1272,7 +1323,7 @@ export default function DocumentsIndex() {
</>
)}
{/* 删除按钮 - 需要 document:document:view 权限 */}
{canView && (
{canView && isDeletableFileStatus(historyDoc.fileStatus) && (
<button
type="button"
className="text-xs px-2 py-1 h-7 text-error hover:underline hover:text-red-700"
+33 -8
View File
@@ -25,7 +25,7 @@
* @author 中国烟草AI合同及卷宗审核系统开发团队
*/
import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/node";
import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs, redirect } from "@remix-run/node";
import { useState, useEffect, useRef } from "react";
import { useNavigate, useLoaderData, useFetcher } from "@remix-run/react";
import reviewsStyles from "~/styles/reviews.css?url";
@@ -345,17 +345,24 @@ export const handle = {
};
export async function loader({ request }: LoaderFunctionArgs): Promise<Response> {
try {
const url = new URL(request.url);
const id = url.searchParams.get('id') || '';
const previousRoute = url.searchParams.get('previousRoute') || '';
const url = new URL(request.url);
const id = url.searchParams.get('id') || '';
const previousRoute = url.searchParams.get('previousRoute') || '';
try {
if (!id) {
return Response.json({ result: false, message: '文件ID不能为空', previousRoute });
}
const { getUserSession } = await import("~/api/login/auth.server");
const { userInfo, frontendJWT } = await getUserSession(request);
if (!frontendJWT || !userInfo?.role) {
throw redirect('/login');
}
const { requireRoutePermission } = await import("~/api/auth/check-route-permission.server");
await requireRoutePermission('/reviewsTest', userInfo.role, frontendJWT);
const reviewData = await getReviewPoints_fromApi(id, request);
if ('error' in reviewData && reviewData.error) {
@@ -382,11 +389,25 @@ export async function loader({ request }: LoaderFunctionArgs): Promise<Response>
detailMode: 'leaudit',
});
} catch (error) {
if (error instanceof Response) {
if (error.status === 401) {
throw redirect('/login');
}
if (error.status === 403) {
return Response.json({
result: false,
message: '当前账号没有评查详情访问权限,请联系管理员开通文档查看权限。',
previousRoute,
}, { status: 403 });
}
}
console.error('[reviewsTest loader] Failed to load review data:', error);
return Response.json({
result: false,
message: `获取评查数据失败: ${error instanceof Error ? error.message : '未知错误'}`,
previousRoute: '',
previousRoute,
});
}
}
@@ -621,6 +642,11 @@ export default function ReviewDetails() {
};
const handleDownloadFile = async () => {
if (!previewPath) {
toastService.warning('当前文档暂无可下载原文件');
return;
}
try {
const downloadUrl = `/api/pdf-proxy?path=${encodeURIComponent(previewPath)}`;
const response = await axios.get(downloadUrl, { responseType: 'blob' });
@@ -828,8 +854,7 @@ export default function ReviewDetails() {
if (result.success) {
toastService.success('评查结果已确认,文档审核状态已更新');
// 导航到文档列表页
navigate('/documents/list');
navigate(getReturnUrl());
} else {
console.error('确认评查结果失败:', result.error);
toastService.error(`确认评查结果失败: ${result.error || '未知错误'}`);
+213 -51
View File
@@ -222,6 +222,7 @@ function emptyFieldDraft(group = '基础信息'): FieldDraft {
name: '',
type: 'verbatim',
multipleEntities: false,
allowed: [],
requiredFrom: 'draft',
description: '',
};
@@ -245,9 +246,13 @@ function emptyVisualDraft(type = '签章'): VisualDraft {
name: '',
type,
required: 'true',
requiredFrom: '',
signerRoles: [],
signatureTypes: [],
privateSealRestricted: false,
expectedMatchField: '',
expectedMatchAlternatives: [],
prompt: '',
};
}
@@ -507,6 +512,7 @@ export default function RulesTestDetail() {
const [fieldDraft, setFieldDraft] = useState<FieldDraft>(emptyFieldDraft(pack.fields[0]?.group || '基础信息'));
const [documentDraft, setDocumentDraft] = useState<DocumentDraft>(emptyDocumentDraft());
const [visualDraft, setVisualDraft] = useState<VisualDraft>(emptyVisualDraft(pack.visualElements[0]?.type || '签章'));
const [versionItems, setVersionItems] = useState<RuleVersionItem[]>(versions);
const [selectedRollbackVersionId, setSelectedRollbackVersionId] = useState('');
const [dependencyDialogOpen, setDependencyDialogOpen] = useState(false);
const [dependencySearch, setDependencySearch] = useState('');
@@ -524,6 +530,7 @@ export default function RulesTestDetail() {
setFields(pack.fields);
setSubDocuments(pack.subDocuments);
setVisualElements(pack.visualElements);
setVersionItems(versions);
setSelectedRuleKey(requestedRuleId || ruleKey(pack.rules[0] || { id: '', ruleId: '' }));
setEditor(null);
setDependencyDialogOpen(false);
@@ -535,7 +542,7 @@ export default function RulesTestDetail() {
setDraftSaved(false);
setSaveMessage('');
setSaveError('');
}, [pack.id, requestedRuleId]);
}, [pack.currentVersionId, pack.fallbackVersionId, pack.id, pack.resolvedVersionId, pack.yamlSource, requestedRuleId, versions]);
const currentRule = useMemo(() => {
return rules.find(rule => rule.id === selectedRuleKey || rule.ruleId === selectedRuleKey) || rules[0];
@@ -640,46 +647,47 @@ export default function RulesTestDetail() {
const rulesById = useMemo(() => new Map(rules.map(rule => [rule.ruleId || rule.id, rule])), [rules]);
const saveButtonBusy = saveFetcher.state !== 'idle';
const latestDraftVersion = useMemo(
() => versions.find((item) => !['published', 'rollback'].includes(item.status)),
[versions],
() => versionItems.find((item) => !['published', 'rollback'].includes(item.status)),
[versionItems],
);
const rollbackVersionOptions = useMemo(
() => versions,
[versions],
() => versionItems,
[versionItems],
);
const rollbackOptions = useMemo(
() => rollbackVersionOptions.filter((item) => item.id !== pack.currentVersionId),
[rollbackVersionOptions, pack.currentVersionId],
);
const rollbackTargetVersion = useMemo(
() => rollbackOptions.find((item) => String(item.id) === selectedRollbackVersionId) || rollbackOptions[0] || null,
[rollbackOptions, selectedRollbackVersionId],
);
const packFilterMainType = pack.businessType || pack.mainType;
const currentResolvedVersion = useMemo(
() => {
if (pack.currentVersionId) {
return versions.find((item) => item.id === pack.currentVersionId) || null;
return versionItems.find((item) => item.id === pack.currentVersionId) || null;
}
if (pack.fallbackVersionId) {
return versions.find((item) => item.id === pack.fallbackVersionId) || null;
return versionItems.find((item) => item.id === pack.fallbackVersionId) || null;
}
return null;
},
[pack.currentVersionId, pack.fallbackVersionId, versions],
[pack.currentVersionId, pack.fallbackVersionId, versionItems],
);
const versionStatusLabel = (status: string | undefined) => {
const normalized = String(status || '').trim().toLowerCase();
if (normalized === 'published') return '已发布';
if (normalized === 'rollback') return '回滚版本';
if (normalized === 'rollback') return '回滚替换';
if (normalized === 'draft') return '草稿';
if (normalized === 'deprecated') return '已废弃';
if (normalized === 'deprecated') return '历史废弃版本';
return status || '-';
};
useEffect(() => {
setSelectedRollbackVersionId('');
}, [pack.id, pack.currentVersionId, versions.length]);
const hasSelectedVersion = rollbackOptions.some((item) => String(item.id) === selectedRollbackVersionId);
const selectedIsCurrent = selectedRollbackVersionId !== '' && String(pack.currentVersionId || '') === selectedRollbackVersionId;
if (hasSelectedVersion && !selectedIsCurrent) {
return;
}
setSelectedRollbackVersionId(rollbackOptions[0] ? String(rollbackOptions[0].id) : '');
}, [pack.currentVersionId, rollbackOptions, selectedRollbackVersionId]);
useEffect(() => {
if (!saveFetcher.data) return;
@@ -687,12 +695,35 @@ export default function RulesTestDetail() {
setDraftSaved(saveFetcher.data.intent === 'save');
setSaveError('');
if (saveFetcher.data.intent === 'save') {
if (saveFetcher.data.versionId) {
setVersionItems((current) => {
const nextVersion: RuleVersionItem = {
id: saveFetcher.data?.versionId || 0,
ruleSetId: current[0]?.ruleSetId || 0,
versionNo: saveFetcher.data?.versionNo || '-',
status: 'draft',
ossUrl: current.find((item) => item.id === saveFetcher.data?.versionId)?.ossUrl || '',
changeNote: 'rulesTest.detail 保存评查点草稿',
publishedAt: current.find((item) => item.id === saveFetcher.data?.versionId)?.publishedAt || null,
};
const existed = current.some((item) => item.id === nextVersion.id);
if (existed) {
return current.map((item) => (item.id === nextVersion.id ? { ...item, ...nextVersion } : item));
}
return [nextVersion, ...current];
});
}
setSaveMessage(saveFetcher.data.versionNo
? `规则草稿已保存为版本 ${saveFetcher.data.versionNo}`
: saveFetcher.data.message || '规则草稿已保存');
} else {
setSaveMessage(saveFetcher.data.message || (saveFetcher.data.intent === 'publish' ? '规则版本已发布' : '规则版本已回滚'));
return;
}
if (saveFetcher.data.intent === 'publish') {
setSaveMessage(saveFetcher.data.message || (saveFetcher.data.intent === 'publish' ? '规则版本已发布' : '规则版本已回滚'));
revalidator.revalidate();
return;
}
setSaveMessage(saveFetcher.data.message || '规则版本已回滚');
revalidator.revalidate();
return;
}
@@ -839,6 +870,7 @@ export default function RulesTestDetail() {
group: fieldDraft.group || '未分组',
requiredFrom: fieldDraft.requiredFrom || 'draft',
type: fieldDraft.type || 'verbatim',
allowed: fieldDraft.type === 'enum' ? (fieldDraft.allowed || []).map((item) => String(item || '').trim()).filter(Boolean) : [],
description: fieldDraft.description || '',
};
setFields((current) => editor.mode === 'edit'
@@ -879,6 +911,7 @@ export default function RulesTestDetail() {
group: field.group || '未分组',
requiredFrom: field.requiredFrom || '-',
type: field.type || 'verbatim',
allowed: field.type === 'enum' ? (field.allowed || []).map((item) => String(item || '').trim()).filter(Boolean) : [],
})),
};
setSubDocuments((current) => editor.mode === 'edit'
@@ -927,6 +960,7 @@ export default function RulesTestDetail() {
name: '',
type: 'verbatim',
multipleEntities: false,
allowed: [],
requiredFrom: '-',
description: '',
},
@@ -951,15 +985,20 @@ export default function RulesTestDetail() {
const previousVisual = editor.mode === 'edit'
? visualElements.find((item) => item.id === editor.id)
: undefined;
const normalizedType = visualDraft.type || '签章';
const normalizedVisual: VisualElementSummary = {
...visualDraft,
id: visualDraft.id || makeId('visual'),
name: visualDraft.name || visualDraft.id,
type: visualDraft.type || '签章',
type: normalizedType,
required: visualDraft.required || 'true',
signerRoles: (visualDraft.signerRoles || []).filter(Boolean),
requiredFrom: visualDraft.requiredFrom || '',
signerRoles: normalizedType === '签名' ? (visualDraft.signerRoles || []).filter(Boolean) : [],
signatureTypes: (visualDraft.signatureTypes || []).filter(Boolean),
privateSealRestricted: Boolean(visualDraft.privateSealRestricted),
privateSealRestricted: normalizedType === '签名' ? Boolean(visualDraft.privateSealRestricted) : false,
expectedMatchField: visualDraft.expectedMatchField || '',
expectedMatchAlternatives: (visualDraft.expectedMatchAlternatives || []).filter(Boolean),
prompt: normalizedType === '骑缝章' ? (visualDraft.prompt || '') : '',
};
setVisualElements((current) => editor.mode === 'edit'
? current.map((item) => (item.id === editor.id ? normalizedVisual : item))
@@ -974,7 +1013,7 @@ export default function RulesTestDetail() {
previousVisual ? { from: `visual.${previousVisual.id}`, to: `visual.${normalizedVisual.id}` } : null,
previousVisual ? { from: `visual.${previousVisual.name || previousVisual.id}`, to: `visual.${normalizedVisual.name || normalizedVisual.id}` } : null,
].filter(Boolean) as Array<{ from: string; to: string }>,
[`visual.${normalizedVisual.id}`],
[`visual.${normalizedVisual.name || normalizedVisual.id}`],
);
setEditor(null);
};
@@ -1027,6 +1066,7 @@ export default function RulesTestDetail() {
};
const rollbackRuleVersion = () => {
const rollbackTargetVersion = rollbackOptions.find((item) => String(item.id) === selectedRollbackVersionId) || null;
if (!rollbackTargetVersion) {
setSaveError('当前没有可回滚的历史可用版本。');
setSaveMessage('');
@@ -1119,7 +1159,7 @@ export default function RulesTestDetail() {
</button>
<select
className="rules-version-select"
value={rollbackTargetVersion ? String(rollbackTargetVersion.id) : selectedRollbackVersionId}
value={selectedRollbackVersionId}
onChange={(event) => setSelectedRollbackVersionId(event.target.value)}
disabled={saveButtonBusy || rollbackVersionOptions.length === 0}
>
@@ -1131,11 +1171,11 @@ export default function RulesTestDetail() {
</option>
))}
</select>
<button type="button" className="ant-btn ant-btn-default" disabled={saveButtonBusy || !rollbackTargetVersion} onClick={rollbackRuleVersion}>
<button type="button" className="ant-btn ant-btn-default" disabled={saveButtonBusy || !selectedRollbackVersionId} onClick={rollbackRuleVersion}>
<i className="ri-history-line mr-1.5"></i>
</button>
<button type="button" className="ant-btn ant-btn-primary" disabled={!currentRule} onClick={() => currentRule && openRuleEditor(currentRule)}>
<i className="ri-edit-line mr-1.5"></i>
<button type="button" className="ant-btn ant-btn-primary" onClick={() => currentRule ? openRuleEditor(currentRule) : openRuleEditor()}>
<i className={`${currentRule ? 'ri-edit-line' : 'ri-add-line'} mr-1.5`}></i>{currentRule ? '编辑评查点' : '新增评查点'}
</button>
</div>
</div>
@@ -1305,7 +1345,7 @@ export default function RulesTestDetail() {
<div key={item.id} className="config-item-card">
<div className="config-item-main">
<strong>{item.name || item.id}</strong>
<span>{item.type} / {item.id}</span>
<span>{item.type}</span>
<span>{requiredFromLabel(String(item.required))}{item.signatureTypes && item.signatureTypes.length > 0 ? ` · ${item.signatureTypes.join('、')}` : ''}</span>
</div>
<div className="config-item-actions">
@@ -1421,9 +1461,101 @@ export default function RulesTestDetail() {
</Card>
</>
) : (
<Card className="ant-card">
<div className="empty-state"></div>
</Card>
<>
<Card className="ant-card">
<div className="empty-state">
/ /
</div>
</Card>
<Card className="ant-card" title="抽取字段">
<div className="config-section-tools">
<span className="config-section-tip"></span>
<button type="button" className="ant-btn ant-btn-default" onClick={() => openFieldEditor()}>
<i className="ri-add-line mr-1.5"></i>
</button>
</div>
{fields.length > 0 ? (
<div className="config-item-list">
{fields.map((field) => (
<div key={field.id} className="config-item-card">
<div className="config-item-main">
<strong>{field.name}</strong>
<span>{field.group || '未分组'} / {fieldTypeLabel(field.type)}</span>
<span>
{requiredFromLabel(field.requiredFrom || '-')}
{field.type === 'enum' && field.allowed && field.allowed.length > 0 ? ` · ${field.allowed.join('、')}` : ''}
{field.description ? ` · ${field.description}` : ''}
</span>
</div>
<div className="config-item-actions">
<button type="button" className="ant-btn ant-btn-default" onClick={() => openFieldEditor(field)}></button>
<button type="button" className="ant-btn ant-btn-default" onClick={() => removeField(field.id)}></button>
</div>
</div>
))}
</div>
) : (
<div className="empty-state"></div>
)}
</Card>
<Card className="ant-card" title="子文档 / 文书">
<div className="config-section-tools">
<span className="config-section-tip"></span>
<button type="button" className="ant-btn ant-btn-default" onClick={() => openDocumentEditor()}>
<i className="ri-add-line mr-1.5"></i>
</button>
</div>
{subDocuments.length > 0 ? (
<div className="config-item-list">
{subDocuments.map((document) => (
<div key={document.id} className="config-item-card">
<div className="config-item-main">
<strong>{document.name}</strong>
<span>{document.id} / {requiredFromLabel(document.required || '-')}</span>
<span>{document.fields.length} {document.groups.length > 0 ? ` · ${document.groups.join('、')}` : ''}</span>
</div>
<div className="config-item-actions">
<button type="button" className="ant-btn ant-btn-default" onClick={() => openDocumentEditor(document)}></button>
<button type="button" className="ant-btn ant-btn-default" onClick={() => removeDocument(document.id)}></button>
</div>
</div>
))}
</div>
) : (
<div className="empty-state"></div>
)}
</Card>
<Card className="ant-card" title="视觉要素">
<div className="config-section-tools">
<span className="config-section-tip"></span>
<button type="button" className="ant-btn ant-btn-default" onClick={() => openVisualEditor()}>
<i className="ri-add-line mr-1.5"></i>
</button>
</div>
{visualElements.length > 0 ? (
<div className="config-item-list">
{visualElements.map((item) => (
<div key={item.id} className="config-item-card">
<div className="config-item-main">
<strong>{item.name || item.id}</strong>
<span>{item.type}</span>
<span>{requiredFromLabel(String(item.required))}{item.signatureTypes && item.signatureTypes.length > 0 ? ` · ${item.signatureTypes.join('、')}` : ''}</span>
</div>
<div className="config-item-actions">
<button type="button" className="ant-btn ant-btn-default" onClick={() => openVisualEditor(item)}></button>
<button type="button" className="ant-btn ant-btn-default" onClick={() => removeVisual(item.id)}></button>
</div>
</div>
))}
</div>
) : (
<div className="empty-state"></div>
)}
</Card>
</>
)}
</div>
@@ -1603,7 +1735,7 @@ export default function RulesTestDetail() {
<div className="drawer-grid">
<label>
<span></span>
<select value={fieldDraft.type} onChange={(event) => setFieldDraft({ ...fieldDraft, type: event.target.value })}>
<select value={fieldDraft.type} onChange={(event) => setFieldDraft({ ...fieldDraft, type: event.target.value, allowed: event.target.value === 'enum' ? (fieldDraft.allowed || []) : [] })}>
{['verbatim', 'string', 'money', 'date', 'enum', 'number'].map((type) => <option key={type} value={type}>{fieldTypeLabel(type)}</option>)}
</select>
</label>
@@ -1614,6 +1746,16 @@ export default function RulesTestDetail() {
</select>
</label>
</div>
{fieldDraft.type === 'enum' && (
<label>
<span></span>
<input
value={(fieldDraft.allowed || []).join('')}
onChange={(event) => setFieldDraft({ ...fieldDraft, allowed: event.target.value.split(/[,]/).map((item) => item.trim()).filter(Boolean) })}
placeholder="如:有,无;男,女"
/>
</label>
)}
<label>
<span></span>
<textarea value={fieldDraft.description} onChange={(event) => setFieldDraft({ ...fieldDraft, description: event.target.value })} placeholder="描述字段如何抽取、给规则如何引用" />
@@ -1658,12 +1800,19 @@ export default function RulesTestDetail() {
/>
<select
value={field.type || 'verbatim'}
onChange={(event) => updateDocumentField(field.id, { type: event.target.value })}
onChange={(event) => updateDocumentField(field.id, { type: event.target.value, allowed: event.target.value === 'enum' ? (field.allowed || []) : [] })}
>
{['verbatim', 'string', 'money', 'date', 'enum', 'number'].map((type) => (
<option key={type} value={type}>{fieldTypeLabel(type)}</option>
))}
</select>
{field.type === 'enum' && (
<input
value={(field.allowed || []).join('')}
onChange={(event) => updateDocumentField(field.id, { allowed: event.target.value.split(/[,]/).map((item) => item.trim()).filter(Boolean) })}
placeholder="可选值"
/>
)}
<input
value={field.description || ''}
onChange={(event) => updateDocumentField(field.id, { description: event.target.value })}
@@ -1697,7 +1846,16 @@ export default function RulesTestDetail() {
<div className="drawer-grid">
<label>
<span></span>
<select value={visualDraft.type} onChange={(event) => setVisualDraft({ ...visualDraft, type: event.target.value })}>
<select
value={visualDraft.type}
onChange={(event) => setVisualDraft((current) => ({
...current,
type: event.target.value,
signerRoles: event.target.value === '签名' ? current.signerRoles || [] : [],
privateSealRestricted: event.target.value === '签名' ? Boolean(current.privateSealRestricted) : false,
prompt: event.target.value === '骑缝章' ? current.prompt || '' : '',
}))}
>
{['签章', '签名', '骑缝章'].map((type) => <option key={type} value={type}>{type}</option>)}
</select>
</label>
@@ -1709,29 +1867,33 @@ export default function RulesTestDetail() {
</label>
</div>
<label>
<span></span>
<span>{visualDraft.type === '签名' ? '签名类型(逗号分隔)' : '签章类型(逗号分隔)'}</span>
<input
value={(visualDraft.signatureTypes || []).join('')}
onChange={(event) => setVisualDraft({ ...visualDraft, signatureTypes: event.target.value.split(/[,]/).map((item) => item.trim()).filter(Boolean) })}
placeholder="如:合同专用章,公章"
placeholder={visualDraft.type === '签名' ? '如:签名,私章' : '如:合同专用章,公章'}
/>
</label>
<label>
<span></span>
<input
value={(visualDraft.signerRoles || []).join('')}
onChange={(event) => setVisualDraft({ ...visualDraft, signerRoles: event.target.value.split(/[,]/).map((item) => item.trim()).filter(Boolean) })}
placeholder="如:甲方,乙方,承办人"
/>
</label>
<label className="drawer-checkbox-row">
<input
type="checkbox"
checked={Boolean(visualDraft.privateSealRestricted)}
onChange={(event) => setVisualDraft({ ...visualDraft, privateSealRestricted: event.target.checked })}
/>
<span> / </span>
</label>
{visualDraft.type === '签名' && (
<>
<label>
<span></span>
<input
value={(visualDraft.signerRoles || []).join('')}
onChange={(event) => setVisualDraft({ ...visualDraft, signerRoles: event.target.value.split(/[,]/).map((item) => item.trim()).filter(Boolean) })}
placeholder="如:甲方,乙方,承办人"
/>
</label>
<label className="drawer-checkbox-row">
<input
type="checkbox"
checked={Boolean(visualDraft.privateSealRestricted)}
onChange={(event) => setVisualDraft({ ...visualDraft, privateSealRestricted: event.target.checked })}
/>
<span> / </span>
</label>
</>
)}
<div className="drawer-actions">
<Button type="default" onClick={() => setEditor(null)}></Button>
<Button type="primary" onClick={saveVisual}></Button>
+21 -17
View File
@@ -7,8 +7,8 @@ import { FilterPanel, FilterSelect, SearchFilter } from '~/components/ui/FilterP
import { Pagination } from '~/components/ui/Pagination';
import { Table } from '~/components/ui/Table';
import { Tag, type TagColor } from '~/components/ui/Tag';
import { loadRuleConfigPacks } from '~/utils/rules-config-packs.server';
import type { RuleSummary, RuleYamlPack } from '~/utils/rules-yaml-mock.server';
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 = () => [
@@ -31,12 +31,12 @@ type RuleRow = RuleSummary & {
mainType: string;
subtype: string;
yamlName: string;
yamlStatus: RuleYamlPack['sourceStatus'];
yamlStatus: RuleConfigPackSummary['sourceStatus'];
isPlaceholder?: boolean;
};
type LoaderData = {
rows: RuleRow[];
packs: RuleYamlPack[];
filters: {
documentType: string;
mainType: string;
@@ -61,7 +61,7 @@ function unique(values: string[]): string[] {
return Array.from(new Set(values.filter(Boolean)));
}
function resolveDocumentScope(pack: Pick<RuleYamlPack, 'documentType' | 'mainType' | 'moduleType'>): string {
function resolveDocumentScope(pack: Pick<RuleConfigPackSummary, 'documentType' | 'mainType' | 'moduleType'>): string {
const values = [pack.documentType, pack.mainType, pack.moduleType].join(' ');
if (values.includes('合同')) return '合同';
if (values.includes('案卷') || values.includes('卷宗') || values.includes('行政处罚') || values.includes('行政许可')) {
@@ -71,7 +71,7 @@ function resolveDocumentScope(pack: Pick<RuleYamlPack, 'documentType' | 'mainTyp
return pack.documentType || pack.mainType || pack.moduleType || '未分类';
}
function resolveBusinessType(pack: Pick<RuleYamlPack, 'businessType' | 'mainType'>): string {
function resolveBusinessType(pack: Pick<RuleConfigPackSummary, 'businessType' | 'mainType'>): string {
return pack.businessType || pack.mainType || '';
}
@@ -99,7 +99,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
pageSize: [10, 20, 30, 50].includes(requestedPageSize) ? requestedPageSize : 10
};
const packs = await loadRuleConfigPacks(request);
const packs = await loadRuleConfigPackSummaries(request);
const packScopes = packs.map(pack => ({
pack,
scope: resolveDocumentScope(pack),
@@ -158,11 +158,11 @@ export async function loader({ request }: LoaderFunctionArgs) {
moduleType: pack.moduleType,
mainType: resolveBusinessType(pack),
subtype: pack.subtype,
yamlName: pack.metadata.name || '待配置 YAML',
yamlName: pack.yamlName || '待配置 YAML',
yamlStatus: pack.sourceStatus,
id: `${pack.id}-empty`,
ruleId: '-',
name: '暂无规则配置',
name: `${pack.subtype}待配置`,
group: '待配置',
risk: '-',
score: '-',
@@ -176,7 +176,10 @@ export async function loader({ request }: LoaderFunctionArgs) {
stageCount: 0,
appliesIn: [],
prompt: '',
description: '当前文档类型已保留规则列表与 YAML 配置页流程,等待后续接入规则文件。'
description: pack.sourceStatus === 'missing'
? '当前规则集已建立,但生效版本正文暂未成功加载,请进入配置页检查并重新保存。'
: '当前子类型还没有正式评查点,请进入配置页补充字段、子文档与评查规则。',
isPlaceholder: true,
}];
}
@@ -188,7 +191,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
moduleType: pack.moduleType,
mainType: resolveBusinessType(pack),
subtype: pack.subtype,
yamlName: pack.metadata.name,
yamlName: pack.yamlName,
yamlStatus: pack.sourceStatus
}));
}).filter(row => {
@@ -207,7 +210,6 @@ export async function loader({ request }: LoaderFunctionArgs) {
return Response.json({
rows,
packs,
filters: {
...filters,
page: currentPage
@@ -292,7 +294,7 @@ export default function RulesTestList() {
render: (_: unknown, record: RuleRow) => (
<div className="rule-name">
<strong>{record.name}</strong>
<span>{record.ruleId}</span>
<span>{record.isPlaceholder ? record.description : record.ruleId}</span>
</div>
)
},
@@ -320,7 +322,9 @@ export default function RulesTestList() {
width: '8%',
align: 'center' as const,
render: (_: unknown, record: RuleRow) => (
<Tag color={riskColor(record.risk)} size="sm">{record.risk}</Tag>
<Tag color={record.isPlaceholder ? (record.yamlStatus === 'missing' ? 'orange' : 'blue') : riskColor(record.risk)} size="sm">
{record.isPlaceholder ? (record.yamlStatus === 'missing' ? '待修复' : '待配置') : record.risk}
</Tag>
)
},
{
@@ -329,7 +333,7 @@ export default function RulesTestList() {
width: '8%',
align: 'center' as const,
render: (_: unknown, record: RuleRow) => (
<Tag color="gray" size="sm">{record.score}</Tag>
<Tag color="gray" size="sm">{record.isPlaceholder ? '-' : record.score}</Tag>
)
},
{
@@ -337,7 +341,7 @@ export default function RulesTestList() {
key: 'dependencies',
width: '20%',
render: (_: unknown, record: RuleRow) => (
<span>{record.dependencies.length > 0 ? record.dependencies.slice(0, 3).join('、') : '-'}</span>
<span>{record.isPlaceholder ? '先进入配置页补规则与依赖' : (record.dependencies.length > 0 ? record.dependencies.slice(0, 3).join('、') : '-')}</span>
)
},
{
@@ -347,7 +351,7 @@ export default function RulesTestList() {
align: 'center' as const,
render: (_: unknown, record: RuleRow) => (
<Link className="operation-btn" to={`/rulesTest/detail?packId=${encodeURIComponent(record.packId)}&ruleId=${encodeURIComponent(record.ruleId || record.id)}`}>
<i className="ri-settings-3-line"></i>
<i className="ri-settings-3-line"></i> {record.isPlaceholder ? '去配置' : '配置'}
</Link>
)
}