fix: align rules list and review detail flows
This commit is contained in:
+56
-118
@@ -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}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user