fix: align rules list and review detail flows

This commit is contained in:
wren
2026-05-06 10:35:57 +08:00
parent 99fce169cb
commit 22ef99754c
9 changed files with 257 additions and 172 deletions
+56 -118
View File
@@ -26,10 +26,10 @@
*/
import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/node";
import { useState, useEffect, useCallback, useRef } from "react";
import { useNavigate, useLoaderData, useFetcher, useRevalidator } from "@remix-run/react";
import { useState, useEffect, useRef } from "react";
import { useNavigate, useLoaderData, useFetcher } from "@remix-run/react";
import reviewsStyles from "~/styles/reviews.css?url";
import { getReviewPoints_fromApi, getUnifiedEvaluationResults, updateReviewResult, confirmReviewResults } from "~/api/evaluation_points/reviews";
import { getReviewPoints_fromApi, updateReviewResult, confirmReviewResults } from "~/api/evaluation_points/reviews";
import { toastService } from "~/components/ui/Toast";
import { Modal } from "~/components/ui/Modal";
import { UploadArea, type UploadAreaRef } from "~/components/ui/UploadArea";
@@ -155,6 +155,32 @@ interface ReviewData {
aiAnalysis: AnalysisData;
}
type PreviewDocument = {
path?: string;
attachments?: Array<{
fileRole?: string;
ossUrl?: string;
}>;
};
function resolvePreviewPath(document: PreviewDocument | null | undefined): string {
if (document?.path) {
return document.path;
}
const primaryAttachment = Array.isArray(document?.attachments)
? document.attachments.find((item) => item?.fileRole === 'primary' && item?.ossUrl)
: null;
return primaryAttachment?.ossUrl || '';
}
function resolvePreviewExtension(document: PreviewDocument | null | undefined): string {
const path = resolvePreviewPath(document);
const suffix = path.split('.').pop();
return typeof suffix === 'string' ? suffix.toLowerCase() : '';
}
function buildReviewData(document: any, reviewPoints: ReviewPoint[], statistics: Statistics, reviewInfo: ReviewInfo): ReviewData | null {
if (!document) {
return null;
@@ -222,110 +248,26 @@ export async function loader({ request }: LoaderFunctionArgs) {
// 获取用户会话信息
const { getUserSession } = await import("~/api/login/auth.server");
const { userInfo, frontendJWT } = await getUserSession(request);
// 🆕 使用新的统一API获取评查点数据
// 先尝试新的统一评查接口
const unifiedData = await getUnifiedEvaluationResults(id, request);
// 如果统一接口返回错误或 flow_type 为 legacy,直接走新后端聚合接口
if ('error' in unifiedData || !unifiedData.flow_type) {
console.log("[Reviews Loader] 统一接口不可用,直接尝试 review-points 聚合接口...");
const reviewData = await getReviewPoints_fromApi(id, request);
if ('error' in reviewData && reviewData.error) {
console.error("[Reviews Loader] 获取评查点数据错误:", reviewData.error);
return Response.json({ result: false, message: reviewData.error });
}
if ('document' in reviewData && 'data' in reviewData && 'reviewInfo' in reviewData && 'stats' in reviewData) {
return Response.json({
previousRoute: previousRoute,
document: reviewData.document,
reviewPoints: reviewData.data,
reviewInfo: reviewData.reviewInfo,
statistics: reviewData.stats,
comparison_document: reviewData.comparison_document,
userInfo,
frontendJWT,
flowType: 'legacy',
scoredResults: null,
scoredSummary: null
});
}
// reviewsTest 正式链路统一只走新后端聚合接口,避免旧统一接口 404 噪音。
const reviewData = await getReviewPoints_fromApi(id, request);
if ('error' in reviewData && reviewData.error) {
console.error("[Reviews Loader] 获取评查点数据错误:", reviewData.error);
return Response.json({ result: false, message: reviewData.error });
}
// 统一接口成功返回,判断流程类型
if (unifiedData.flow_type === 'graphrag') {
// 先获取文档基本信息(统一接口不返回文档内容)
const reviewData = await getReviewPoints_fromApi(id, request);
// 合并已评查的 reviewPoints + 未涉及的评查点
const existingPoints = ('data' in reviewData && !('error' in reviewData)) ? reviewData.data : [];
const notApplicablePoints = (unifiedData.results || [])
.filter((r: any) => r.result_type === 'not_applicable')
.map((r: any) => ({
id: `na-${r.evaluation_point_id}`,
documentId: id,
pointId: r.evaluation_point_id,
editAuditStatusId: '',
editAuditStatus: '',
editAuditStatusMessage: '',
title: '该评查点未涉及',
pointName: r.name || '',
pointCode: r.code || '',
groupName: '',
status: 'notApplicable',
content: {},
contentPage: {},
suggestion: r.ai_suggestion || '该评查点未涉及',
result: null,
score: r.score || 0,
finalScore: null,
machineScore: 0,
postAction: '',
}));
const allReviewPoints = [...existingPoints, ...notApplicablePoints];
return Response.json({
previousRoute: previousRoute,
document: ('document' in reviewData && !('error' in reviewData)) ? reviewData.document : null,
reviewPoints: allReviewPoints,
reviewInfo: { reviewTime: unifiedData.evaluated_at, reviewModel: 'GraphRAG', ruleGroup: '', result: '', issueCount: unifiedData.summary?.total_points || 0 },
statistics: {
total: unifiedData.summary?.total_points || 0,
success: unifiedData.summary?.passed_count || 0,
warning: unifiedData.summary?.failed_count || 0,
error: 0,
notApplicable: unifiedData.summary?.not_applicable_count || 0,
score: unifiedData.summary?.total_score || 0
},
comparison_document: ('comparison_document' in reviewData && !('error' in reviewData)) ? reviewData.comparison_document : null,
userInfo,
frontendJWT,
flowType: 'graphrag',
scoredResults: unifiedData.results,
scoredSummary: unifiedData.summary
});
} else {
// legacy 流程但统一接口可用,也统一走 review-points 聚合接口
const reviewData = await getReviewPoints_fromApi(id, request);
if ('error' in reviewData && reviewData.error) {
return Response.json({ result: false, message: reviewData.error });
}
return Response.json({
previousRoute: previousRoute,
document: reviewData.document,
reviewPoints: reviewData.data,
reviewInfo: reviewData.reviewInfo,
statistics: reviewData.stats,
comparison_document: reviewData.comparison_document,
userInfo,
frontendJWT,
flowType: 'legacy',
scoredResults: null,
scoredSummary: null
});
}
return Response.json({
previousRoute: previousRoute,
document: reviewData.document,
reviewPoints: reviewData.data,
reviewInfo: reviewData.reviewInfo,
statistics: reviewData.stats,
comparison_document: reviewData.comparison_document,
userInfo,
frontendJWT,
flowType: 'legacy',
scoredResults: null,
scoredSummary: null
});
} catch (error) {
console.error('[Reviews Loader] 获取评查数据失败:', error);
console.error('[Reviews Loader] 错误堆栈:', error instanceof Error ? error.stack : '无堆栈信息');
@@ -431,7 +373,7 @@ export default function ReviewDetails() {
const [isLoading, setIsLoading] = useState(false); // 已经通过loader加载了数据,不需要再显示加载状态
const [rightActiveTab, setRightActiveTab] = useState<'result' | 'fields' | 'info'>('result');
const [reviewData, setReviewData] = useState<ReviewData | null>(fallbackReviewData);
const [activeReviewPointResultId, setActiveReviewPointResultId] = useState<string | null>(null);
const [activeReviewPointResultId, setActiveReviewPointResultId] = useState<string | number | null>(null);
const [targetPage, setTargetPage] = useState<number | undefined>(undefined);
const [charPositions, setCharPositions] = useState<Array<{ box: number[][], char: string, score: number }> | undefined>(undefined);
const [highlightValue, setHighlightValue] = useState<string | undefined>(undefined);
@@ -443,7 +385,7 @@ export default function ReviewDetails() {
const [showCompareOverlay, setShowCompareOverlay] = useState(false);
// 一键替换(DOCX Collabora 使用)
const [aiSuggestionReplace, setAiSuggestionReplace] = useState<{
const [aiSuggestionReplace] = useState<{
searchText: string;
replaceText: string;
pageNumber: number;
@@ -456,7 +398,8 @@ export default function ReviewDetails() {
const [isUploading, setIsUploading] = useState(false);
const [showComparison, setShowComparison] = useState(false);
const uploadAreaRef = useRef<UploadAreaRef>(null);
const revalidator = useRevalidator();
const previewPath = resolvePreviewPath(document);
const previewExtension = resolvePreviewExtension(document);
// 结构比对按钮显示条件:fileInfo.type 包含 '1'
const showComparisonButton = (document as any)?.type?.toString().includes('1');
@@ -524,7 +467,7 @@ export default function ReviewDetails() {
};
// 从左栏选择评查点
const handleRuleSelect = (id: string) => {
const handleRuleSelect = (id: string | number) => {
setActiveReviewPointResultId(id);
setRightActiveTab('result');
@@ -578,7 +521,7 @@ export default function ReviewDetails() {
}
};
const handleReviewPointSelect = (reviewPointId: string, page?: number, charPos?: Array<{ box: number[][], char: string, score: number }>, value?: string) => {
const handleReviewPointSelect = (reviewPointId: string | number, page?: number, charPos?: Array<{ box: number[][], char: string, score: number }>, value?: string) => {
// 如果点击的是相同的评查点,但有page参数,先重置targetPage以确保useEffect能够触发
if (reviewPointId === activeReviewPointResultId && page) {
setTargetPage(undefined);
@@ -600,11 +543,6 @@ export default function ReviewDetails() {
}
};
// 处理AI建议替换(保留空实现,右栏详情卡片中DOCX替换需要)
const handleAiSuggestionReplace = (_searchText: string, _replaceText: string, _pageNumber: number) => {
// PDF 文件不支持替换,暂不实现
};
// 刷新评审数据
// async function refreshReviewData(documentId: string) {
// // 设置加载状态
@@ -942,9 +880,9 @@ export default function ReviewDetails() {
{/* 中栏:PDF 预览 */}
{/* 中栏:文件预览(根据文件类型切换) */}
<section className="flex flex-col min-h-0 bg-slate-100">
{document?.path?.split('.').pop()?.toLowerCase() === 'docx' ? (
{previewExtension === 'docx' ? (
<DocxPreviewTest
filePath={document?.path || ''}
filePath={previewPath}
targetPage={targetPage}
charPositions={charPositions}
activeReviewPointResultId={activeReviewPointResultId}
@@ -955,7 +893,7 @@ export default function ReviewDetails() {
/>
) : (
<PdfPreviewTest
filePath={document?.path || ''}
filePath={previewPath}
targetPage={targetPage}
charPositions={charPositions}
activeReviewPointResultId={activeReviewPointResultId}
+30 -13
View File
@@ -61,6 +61,16 @@ function unique(values: string[]): string[] {
return Array.from(new Set(values.filter(Boolean)));
}
function resolveDocumentScope(pack: Pick<RuleYamlPack, '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('行政许可')) {
return '案卷';
}
if (values.includes('公文')) return '内部公文';
return pack.documentType || pack.mainType || pack.moduleType || '未分类';
}
function riskColor(risk: string): TagColor {
if (risk === 'high') return 'red';
if (risk === 'medium') return 'orange';
@@ -86,28 +96,36 @@ export async function loader({ request }: LoaderFunctionArgs) {
};
const packs = await loadRuleConfigPacks(request);
const documentTypes = unique(packs.map(pack => pack.documentType));
const inferredDocumentType = packs.find(pack => pack.mainType === requestedFilters.mainType)?.documentType || '';
const currentDocumentType = documentTypes.includes(requestedFilters.documentType)
? requestedFilters.documentType
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 => item.pack.mainType === 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: packs.some(pack => pack.documentType === currentDocumentType && pack.mainType === requestedFilters.mainType)
mainType: scopedDocumentPacks.some(pack => pack.mainType === requestedFilters.mainType)
? requestedFilters.mainType
: '',
subtype: packs.some(pack =>
pack.documentType === currentDocumentType &&
subtype: scopedDocumentPacks.some(pack =>
(!requestedFilters.mainType || pack.mainType === requestedFilters.mainType) &&
pack.subtype === requestedFilters.subtype
)
? requestedFilters.subtype
: ''
};
const scopedByMainTypePacks = packs.filter(pack =>
pack.documentType === scopedFilters.documentType &&
(!scopedFilters.mainType || pack.mainType === scopedFilters.mainType)
const scopedByMainTypePacks = scopedDocumentPacks.filter(pack =>
!scopedFilters.mainType || pack.mainType === scopedFilters.mainType
);
const subtypeOptions = unique(scopedByMainTypePacks.map(pack => pack.subtype));
const ruleGroupSourcePacks = scopedFilters.subtype
@@ -122,8 +140,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
...scopedFilters,
ruleGroup: ruleGroupOptions.includes(requestedFilters.ruleGroup) ? requestedFilters.ruleGroup : ''
};
const visiblePacks = packs.filter(pack =>
pack.documentType === filters.documentType &&
const visiblePacks = scopedDocumentPacks.filter(pack =>
(!filters.mainType || pack.mainType === filters.mainType) &&
(!filters.subtype || pack.subtype === filters.subtype)
);
@@ -196,7 +213,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
pageSize: filters.pageSize,
options: {
documentTypes,
mainTypes: unique(packs.filter(pack => pack.documentType === filters.documentType).map(pack => pack.mainType)),
mainTypes: unique(scopedDocumentPacks.map(pack => pack.mainType)),
subtypes: subtypeOptions,
ruleGroups: ruleGroupOptions
}