fix: restore reviews detail layout and leaudit data wiring
This commit is contained in:
+278
-148
@@ -45,8 +45,7 @@ import { PdfPreviewTest } from "~/components/reviews/previewComponents/PdfPrevie
|
||||
import { DocxPreviewTest } from "~/components/reviews/previewComponents/DocxPreviewTest";
|
||||
import { ComparePreview } from "~/components/reviews/previewComponents/ComparePreview";
|
||||
|
||||
import { type ReviewPoint } from '~/components/reviews';
|
||||
import { messageService } from "~/components/ui/MessageModal";
|
||||
import { type ReviewPoint, type PdfBboxHighlight } from '~/components/reviews';
|
||||
import { loadingBarService } from "~/components/ui/LoadingBar";
|
||||
|
||||
/**
|
||||
@@ -155,6 +154,8 @@ interface ReviewData {
|
||||
aiAnalysis: AnalysisData;
|
||||
}
|
||||
|
||||
type PreviewKind = 'pdf' | 'docx';
|
||||
|
||||
type PreviewDocument = {
|
||||
path?: string;
|
||||
attachments?: Array<{
|
||||
@@ -163,10 +164,14 @@ type PreviewDocument = {
|
||||
}>;
|
||||
};
|
||||
|
||||
interface DefaultPreviewTarget {
|
||||
page?: number;
|
||||
highlightValue?: string;
|
||||
bboxHighlight?: PdfBboxHighlight;
|
||||
}
|
||||
|
||||
function resolvePreviewPath(document: PreviewDocument | null | undefined): string {
|
||||
if (document?.path) {
|
||||
return document.path;
|
||||
}
|
||||
if (document?.path) return document.path;
|
||||
|
||||
const primaryAttachment = Array.isArray(document?.attachments)
|
||||
? document.attachments.find((item) => item?.fileRole === 'primary' && item?.ossUrl)
|
||||
@@ -181,37 +186,124 @@ function resolvePreviewExtension(document: PreviewDocument | null | undefined):
|
||||
return typeof suffix === 'string' ? suffix.toLowerCase() : '';
|
||||
}
|
||||
|
||||
function buildReviewData(document: any, reviewPoints: ReviewPoint[], statistics: Statistics, reviewInfo: ReviewInfo): ReviewData | null {
|
||||
if (!document) {
|
||||
return null;
|
||||
}
|
||||
function isValidQuad(value: unknown): value is [number, number, number, number] {
|
||||
return Array.isArray(value) && value.length === 4 && value.every(item => typeof item === 'number' && Number.isFinite(item));
|
||||
}
|
||||
|
||||
const mockData = getMockReviewData();
|
||||
const typeValue = document.type ?? document.type_id;
|
||||
function hasNonZeroQuad(value: [number, number, number, number]): boolean {
|
||||
return value.some(item => item !== 0);
|
||||
}
|
||||
|
||||
function getReviewPointContentText(value: unknown): string | undefined {
|
||||
if (value == null) return undefined;
|
||||
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
||||
const text = String(value).trim();
|
||||
return text || undefined;
|
||||
}
|
||||
if (typeof value === 'object' && value && 'value' in value) {
|
||||
return getReviewPointContentText((value as { value?: unknown }).value);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getReviewPointFieldPage(point: ReviewPoint, fieldKey: string, rawValue: unknown): number | undefined {
|
||||
const contentPage = point.contentPage?.[fieldKey];
|
||||
const normalizedContentPage = Number(contentPage);
|
||||
if (Number.isFinite(normalizedContentPage) && normalizedContentPage > 0) return normalizedContentPage;
|
||||
|
||||
const inlinePage = typeof rawValue === 'object' && rawValue && 'page' in rawValue
|
||||
? Number((rawValue as { page?: unknown }).page)
|
||||
: NaN;
|
||||
if (Number.isFinite(inlinePage) && inlinePage > 0) return inlinePage;
|
||||
|
||||
const pageNum = point.fieldPositions?.[fieldKey]?.page_num;
|
||||
if (typeof pageNum === 'number' && Number.isFinite(pageNum) && pageNum >= 0) return pageNum + 1;
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getReviewPointFieldBbox(point: ReviewPoint, fieldKey: string, page?: number): PdfBboxHighlight | undefined {
|
||||
const fieldPosition = point.fieldPositions?.[fieldKey];
|
||||
if (!fieldPosition || !isValidQuad(fieldPosition.bbox) || !isValidQuad(fieldPosition.page_box)) return undefined;
|
||||
if (!hasNonZeroQuad(fieldPosition.bbox) || !hasNonZeroQuad(fieldPosition.page_box)) return undefined;
|
||||
|
||||
return {
|
||||
fileInfo: {
|
||||
fileName: document.name || "未知文件名",
|
||||
path: document.path || "未知路径",
|
||||
contractNumber: document.documentNumber || document.document_number || "未知编号",
|
||||
fileSize: document.size ? formatFileSize(document.size) : document.file_size ? formatFileSize(document.file_size) : "未知大小",
|
||||
fileFormat: document.fileType ? document.fileType.toUpperCase() : "未知格式",
|
||||
pageCount: document.pageCount || document.page_count || 0,
|
||||
uploadTime: document.uploadTime || document.created_at || "未知时间",
|
||||
uploadUser: document.uploadUser || "未知用户",
|
||||
auditStatus: document.auditStatus || 0,
|
||||
legalBasis: document.legalBasis || {},
|
||||
fileType: typeValue !== undefined && typeValue !== null ? String(typeValue) : ""
|
||||
},
|
||||
contractInfo: mockData.contractInfo,
|
||||
reviewInfo,
|
||||
statistics,
|
||||
fileContent: mockData.fileContent,
|
||||
reviewPoints,
|
||||
aiAnalysis: mockData.aiAnalysis,
|
||||
fieldKey,
|
||||
bbox: [...fieldPosition.bbox],
|
||||
pageBox: [...fieldPosition.page_box],
|
||||
pageNum: fieldPosition.page_num,
|
||||
page,
|
||||
confidence: fieldPosition.confidence,
|
||||
matchMethod: fieldPosition.match_method,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveDefaultPreviewTarget(point: ReviewPoint, previewKind: PreviewKind): DefaultPreviewTarget {
|
||||
let firstPageCandidate: DefaultPreviewTarget | undefined;
|
||||
let firstPageWithBboxCandidate: DefaultPreviewTarget | undefined;
|
||||
let firstPageWithTextCandidate: DefaultPreviewTarget | undefined;
|
||||
|
||||
for (const [fieldKey, rawValue] of Object.entries(point.content || {})) {
|
||||
const page = getReviewPointFieldPage(point, fieldKey, rawValue);
|
||||
if (!page) continue;
|
||||
|
||||
const highlightValue = getReviewPointContentText(rawValue);
|
||||
const bboxHighlight = getReviewPointFieldBbox(point, fieldKey, page);
|
||||
const candidate: DefaultPreviewTarget = { page, highlightValue, bboxHighlight };
|
||||
|
||||
if (!firstPageCandidate) firstPageCandidate = candidate;
|
||||
if (!firstPageWithBboxCandidate && bboxHighlight) firstPageWithBboxCandidate = candidate;
|
||||
if (!firstPageWithTextCandidate && highlightValue) firstPageWithTextCandidate = candidate;
|
||||
}
|
||||
|
||||
if (previewKind === 'pdf') {
|
||||
return firstPageWithBboxCandidate || firstPageCandidate || {};
|
||||
}
|
||||
|
||||
return firstPageWithTextCandidate || firstPageCandidate || {};
|
||||
}
|
||||
|
||||
interface ReviewsTestLoaderSuccess {
|
||||
previousRoute: string;
|
||||
document: any;
|
||||
reviewPoints: ReviewPoint[];
|
||||
reviewInfo: ReviewInfo;
|
||||
statistics: Statistics;
|
||||
comparison_document: any;
|
||||
userInfo: { sub: string; nick_name: string } | null;
|
||||
frontendJWT: string | null;
|
||||
flowType: 'legacy' | 'leaudit';
|
||||
detailMode: 'legacy' | 'leaudit';
|
||||
}
|
||||
|
||||
interface ReviewsTestLoaderError {
|
||||
result: false;
|
||||
message: string;
|
||||
previousRoute: string;
|
||||
}
|
||||
|
||||
type ReviewsTestLoaderData = ReviewsTestLoaderSuccess | ReviewsTestLoaderError;
|
||||
|
||||
const EMPTY_STATISTICS: Statistics = {
|
||||
total: 0,
|
||||
success: 0,
|
||||
warning: 0,
|
||||
error: 0,
|
||||
notApplicable: 0,
|
||||
score: 0,
|
||||
};
|
||||
|
||||
const EMPTY_REVIEW_INFO: ReviewInfo = {
|
||||
reviewTime: '',
|
||||
reviewModel: '',
|
||||
ruleGroup: '',
|
||||
result: '',
|
||||
issueCount: 0,
|
||||
};
|
||||
|
||||
function isReviewsTestLoaderError(data: ReviewsTestLoaderData): data is ReviewsTestLoaderError {
|
||||
return 'result' in data && data.result === false;
|
||||
}
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
@@ -233,49 +325,53 @@ export const handle = {
|
||||
noPadding: true
|
||||
};
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
export async function loader({ request }: LoaderFunctionArgs): Promise<Response> {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
const id = url.searchParams.get('id') || undefined;
|
||||
const id = url.searchParams.get('id') || '';
|
||||
const previousRoute = url.searchParams.get('previousRoute') || '';
|
||||
// console.log("[Reviews Loader] 开始加载,id:", id, "previousRoute:", previousRoute);
|
||||
|
||||
if (!id) {
|
||||
console.error("[Reviews Loader] 文件ID不能为空");
|
||||
return Response.json({ result: false, message: '文件ID不能为空' });
|
||||
return Response.json({ result: false, message: '文件ID不能为空', previousRoute });
|
||||
}
|
||||
|
||||
// 获取用户会话信息
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { userInfo, frontendJWT } = await getUserSession(request);
|
||||
// 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 });
|
||||
return Response.json({
|
||||
result: false,
|
||||
message: reviewData.error,
|
||||
previousRoute,
|
||||
});
|
||||
}
|
||||
|
||||
return Response.json({
|
||||
previousRoute: previousRoute,
|
||||
previousRoute,
|
||||
document: reviewData.document,
|
||||
reviewPoints: reviewData.data,
|
||||
reviewInfo: reviewData.reviewInfo,
|
||||
statistics: reviewData.stats,
|
||||
statistics: { ...reviewData.stats, notApplicable: reviewData.stats?.notApplicable ?? 0 },
|
||||
comparison_document: reviewData.comparison_document,
|
||||
userInfo,
|
||||
frontendJWT,
|
||||
flowType: 'legacy',
|
||||
scoredResults: null,
|
||||
scoredSummary: null
|
||||
userInfo:
|
||||
userInfo?.sub && userInfo?.nick_name
|
||||
? { sub: userInfo.sub, nick_name: userInfo.nick_name }
|
||||
: null,
|
||||
frontendJWT: frontendJWT ?? null,
|
||||
flowType: 'leaudit',
|
||||
detailMode: 'leaudit',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[Reviews Loader] 获取评查数据失败:', error);
|
||||
console.error('[Reviews Loader] 错误堆栈:', error instanceof Error ? error.stack : '无堆栈信息');
|
||||
return Response.json({ result: false, message: `获取评查数据失败: ${error instanceof Error ? error.message : '未知错误'}` });
|
||||
console.error('[reviewsTest loader] Failed to load review data:', error);
|
||||
return Response.json({
|
||||
result: false,
|
||||
message: `获取评查数据失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
||||
previousRoute: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 添加 action 函数处理需要用户认证的操作
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
try {
|
||||
const formData = await request.formData();
|
||||
@@ -346,36 +442,27 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
|
||||
export default function ReviewDetails() {
|
||||
const navigate = useNavigate();
|
||||
const loaderData = useLoaderData<typeof loader>();
|
||||
const normalizedLoaderData =
|
||||
loaderData &&
|
||||
typeof loaderData === 'object' &&
|
||||
'reviewPoints' in loaderData &&
|
||||
loaderData.reviewPoints &&
|
||||
typeof loaderData.reviewPoints === 'object' &&
|
||||
'data' in loaderData.reviewPoints &&
|
||||
'document' in loaderData.reviewPoints
|
||||
? {
|
||||
...loaderData,
|
||||
document: (loaderData.reviewPoints as any).document,
|
||||
reviewPoints: (loaderData.reviewPoints as any).data,
|
||||
reviewInfo: (loaderData.reviewPoints as any).reviewInfo,
|
||||
statistics: (loaderData.reviewPoints as any).stats,
|
||||
comparison_document: (loaderData.reviewPoints as any).comparison_document,
|
||||
scoring_proposals: (loaderData.reviewPoints as any).scoring_proposals ?? [],
|
||||
}
|
||||
: loaderData;
|
||||
const loaderData = useLoaderData<ReviewsTestLoaderData>();
|
||||
|
||||
const fetcher = useFetcher();
|
||||
const { document, reviewPoints, statistics, reviewInfo, comparison_document, frontendJWT } = normalizedLoaderData as any;
|
||||
const fallbackReviewData = buildReviewData(document, reviewPoints, statistics, reviewInfo);
|
||||
const isLoaderError = isReviewsTestLoaderError(loaderData);
|
||||
const successLoaderData = isLoaderError ? null : loaderData;
|
||||
const document = successLoaderData?.document ?? null;
|
||||
const reviewPoints = successLoaderData?.reviewPoints ?? [];
|
||||
const statistics = successLoaderData?.statistics ?? EMPTY_STATISTICS;
|
||||
const reviewInfo = successLoaderData?.reviewInfo ?? EMPTY_REVIEW_INFO;
|
||||
const comparison_document = successLoaderData?.comparison_document ?? null;
|
||||
const detailMode = successLoaderData?.detailMode ?? 'legacy';
|
||||
const currentUserInfo = successLoaderData?.userInfo ?? null;
|
||||
const frontendJWT = successLoaderData?.frontendJWT ?? null;
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false); // 已经通过loader加载了数据,不需要再显示加载状态
|
||||
const [rightActiveTab, setRightActiveTab] = useState<'result' | 'fields' | 'info'>('result');
|
||||
const [reviewData, setReviewData] = useState<ReviewData | null>(fallbackReviewData);
|
||||
const [reviewData, setReviewData] = useState<ReviewData | 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 [bboxHighlight, setBboxHighlight] = useState<PdfBboxHighlight | undefined>(undefined);
|
||||
const [highlightValue, setHighlightValue] = useState<string | undefined>(undefined);
|
||||
const [pendingUpdate, setPendingUpdate] = useState<{
|
||||
reviewPointResultId: string;
|
||||
@@ -416,32 +503,14 @@ export default function ReviewDetails() {
|
||||
// console.log('评查信息:', reviewInfo);
|
||||
// console.log('比对文档:', comparison_document);
|
||||
// console.log('用户信息:', loaderData.userInfo);
|
||||
// console.log('JWT Token (前20位):', frontendJWT?.substring(0, 20) + '...');
|
||||
// console.groupEnd();
|
||||
// }
|
||||
// }, [loaderData, document, reviewPoints, statistics, reviewInfo, comparison_document, frontendJWT]);
|
||||
// }, [loaderData, document, reviewPoints, statistics, reviewInfo, comparison_document]);
|
||||
|
||||
// loader 数据加载出错
|
||||
useEffect(()=>{
|
||||
loadingBarService.hide();
|
||||
// console.log('[Reviews Component] useEffect检查loaderData:', {
|
||||
// hasResultKey: Object.keys(loaderData).find(key => key === 'result'),
|
||||
// resultValue: loaderData.result,
|
||||
// willNavigateBack: Object.keys(loaderData).find(key => key === 'result') && !loaderData.result
|
||||
// });
|
||||
if(Object.keys(loaderData).find(key => key === 'result') && !loaderData.result){
|
||||
messageService.show({
|
||||
title: '错误',
|
||||
message: loaderData.message,
|
||||
type: 'error',
|
||||
confirmText: '确定',
|
||||
cancelText: '',
|
||||
onConfirm: () => {
|
||||
navigate(-1);
|
||||
}
|
||||
})
|
||||
}
|
||||
},[loaderData, navigate]);
|
||||
},[loaderData]);
|
||||
|
||||
|
||||
// 当文档 ID 变化时,清空高亮相关的状态
|
||||
@@ -450,17 +519,49 @@ export default function ReviewDetails() {
|
||||
setActiveReviewPointResultId(null);
|
||||
setTargetPage(undefined);
|
||||
setCharPositions(undefined);
|
||||
setBboxHighlight(undefined);
|
||||
setHighlightValue(undefined);
|
||||
}
|
||||
}, [document?.id]);
|
||||
|
||||
// 使用 loader 数据同步本地评查页状态,避免首屏空白。
|
||||
// 模拟获取评查数据
|
||||
useEffect(() => {
|
||||
setReviewData(buildReviewData(document, reviewPoints, statistics, reviewInfo));
|
||||
if (!document) return;
|
||||
|
||||
// 构建文件信息对象
|
||||
const fileInfo = {
|
||||
fileName: document.name || "未知文件名",
|
||||
path: document.path || "未知路径",
|
||||
contractNumber: document.documentNumber || document.document_number || "未知编号",
|
||||
fileSize: document.size ? formatFileSize(document.size) : document.file_size ? formatFileSize(document.file_size) : "未知大小",
|
||||
// 文件格式类型
|
||||
fileFormat: document.fileType ? document.fileType.toUpperCase() : "未知格式",
|
||||
pageCount: document.pageCount || document.page_count || 0,
|
||||
uploadTime: document.uploadTime || document.created_at || "未知时间",
|
||||
uploadUser: document.uploadUser || "未知用户",
|
||||
auditStatus: document.auditStatus || 0,
|
||||
legalBasis: document.legalBasis || {},
|
||||
// 文件类型(1:合同,2:卷宗。。。)
|
||||
fileType: document.type || document.type_id ? document.type_id.toString() : ''
|
||||
};
|
||||
|
||||
// 创建包含真实文档数据的评查数据对象
|
||||
const reviewDataObj: ReviewData = {
|
||||
// 使用真实文件信息
|
||||
fileInfo: fileInfo,
|
||||
// 其他字段暂时使用默认值
|
||||
contractInfo: getMockReviewData().contractInfo,
|
||||
reviewInfo: reviewInfo,
|
||||
statistics: statistics,
|
||||
fileContent: getMockReviewData().fileContent,
|
||||
reviewPoints: reviewPoints,
|
||||
aiAnalysis: getMockReviewData().aiAnalysis,
|
||||
};
|
||||
|
||||
|
||||
setReviewData(reviewDataObj);
|
||||
setIsLoading(false);
|
||||
}, [document, reviewPoints, statistics, reviewInfo]);
|
||||
|
||||
const effectiveReviewData = reviewData ?? fallbackReviewData;
|
||||
|
||||
const handleTabChange = (tabKey: 'result' | 'fields' | 'info') => {
|
||||
setRightActiveTab(tabKey);
|
||||
@@ -468,47 +569,36 @@ export default function ReviewDetails() {
|
||||
|
||||
// 从左栏选择评查点
|
||||
const handleRuleSelect = (id: string | number) => {
|
||||
setActiveReviewPointResultId(id);
|
||||
setRightActiveTab('result');
|
||||
|
||||
// 查找评查点并尝试跳转到其页面
|
||||
const point = effectiveReviewData?.reviewPoints.find(p => p.id === id);
|
||||
const point = reviewData?.reviewPoints.find(p => p.id === id);
|
||||
if (point) {
|
||||
console.log('跳转到评查点页面:', point);
|
||||
const page = getFirstPageFromPoint(point);
|
||||
if (page) setTargetPage(page);
|
||||
else setTargetPage(undefined);
|
||||
setCharPositions(undefined);
|
||||
setHighlightValue(undefined);
|
||||
console.log('选择的评查点:', point);
|
||||
const previewKind: PreviewKind = previewExtension === 'docx' ? 'docx' : 'pdf';
|
||||
const defaultTarget = resolveDefaultPreviewTarget(point, previewKind);
|
||||
|
||||
handleReviewPointSelect(
|
||||
id,
|
||||
defaultTarget.page,
|
||||
undefined,
|
||||
defaultTarget.highlightValue,
|
||||
previewKind === 'pdf' ? defaultTarget.bboxHighlight : undefined,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
handleReviewPointSelect(id);
|
||||
};
|
||||
|
||||
// 从评查点中提取第一个有效页码
|
||||
const getFirstPageFromPoint = (point: ReviewPoint): number | undefined => {
|
||||
if (point.content) {
|
||||
for (const data of Object.values(point.content)) {
|
||||
const page = (data as any)?.page;
|
||||
if (page && Number(page) > 0) return Number(page);
|
||||
}
|
||||
}
|
||||
if (point.contentPage) {
|
||||
for (const page of Object.values(point.contentPage)) {
|
||||
if (page && Number(page) > 0) return Number(page);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// 下载文件
|
||||
const handleDownloadFile = async () => {
|
||||
try {
|
||||
const downloadUrl = `/api/pdf-proxy?path=${encodeURIComponent(document?.path || '')}`;
|
||||
const downloadUrl = `/api/pdf-proxy?path=${encodeURIComponent(previewPath)}`;
|
||||
const response = await axios.get(downloadUrl, { responseType: 'blob' });
|
||||
const blobUrl = URL.createObjectURL(response.data);
|
||||
const a = window.document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
a.href = blobUrl;
|
||||
a.download = decodeURIComponent(document?.path?.split('/').pop() || 'document');
|
||||
a.download = decodeURIComponent(previewPath.split('/').pop() || 'document');
|
||||
window.document.body.appendChild(a);
|
||||
a.click();
|
||||
setTimeout(() => {
|
||||
@@ -521,17 +611,19 @@ export default function ReviewDetails() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleReviewPointSelect = (reviewPointId: string | number, 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, nextBboxHighlight?: PdfBboxHighlight) => {
|
||||
// 如果点击的是相同的评查点,但有page参数,先重置targetPage以确保useEffect能够触发
|
||||
if (reviewPointId === activeReviewPointResultId && page) {
|
||||
setTargetPage(undefined);
|
||||
setCharPositions(undefined);
|
||||
setBboxHighlight(undefined);
|
||||
setHighlightValue(undefined);
|
||||
// 使用setTimeout确保状态更新后再设置新的targetPage、charPositions和highlightValue
|
||||
setTimeout(() => {
|
||||
setActiveReviewPointResultId(reviewPointId);
|
||||
setTargetPage(page);
|
||||
setCharPositions(charPos);
|
||||
setBboxHighlight(nextBboxHighlight);
|
||||
setHighlightValue(value);
|
||||
}, 0);
|
||||
} else {
|
||||
@@ -539,10 +631,16 @@ export default function ReviewDetails() {
|
||||
setActiveReviewPointResultId(reviewPointId);
|
||||
setTargetPage(page);
|
||||
setCharPositions(charPos);
|
||||
setBboxHighlight(nextBboxHighlight);
|
||||
setHighlightValue(value);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理AI建议替换(保留空实现,右栏详情卡片中DOCX替换需要)
|
||||
const handleAiSuggestionReplace = (_searchText: string, _replaceText: string, _pageNumber: number) => {
|
||||
// PDF 文件不支持替换,暂不实现
|
||||
};
|
||||
|
||||
// 刷新评审数据
|
||||
// async function refreshReviewData(documentId: string) {
|
||||
// // 设置加载状态
|
||||
@@ -779,7 +877,7 @@ export default function ReviewDetails() {
|
||||
};
|
||||
|
||||
// 获取当前激活的评查点对象
|
||||
const activeReviewPoint = effectiveReviewData?.reviewPoints.find(p => p.id === activeReviewPointResultId) || null;
|
||||
const activeReviewPoint = reviewData?.reviewPoints.find(p => p.id === activeReviewPointResultId) || null;
|
||||
|
||||
// ── 模板上传相关函数 ──
|
||||
const handleOpenReuploadModal = () => {
|
||||
@@ -821,7 +919,7 @@ export default function ReviewDetails() {
|
||||
selectedTemplateFiles[0],
|
||||
(document as any).id,
|
||||
(comparison_document as any)?.comparisonId,
|
||||
frontendJWT || undefined
|
||||
frontendJWT || undefined,
|
||||
);
|
||||
if (uploadResult.error) throw new Error(uploadResult.error);
|
||||
toastService.success('模板文件上传成功,即将返回上一页...');
|
||||
@@ -833,14 +931,6 @@ export default function ReviewDetails() {
|
||||
} finally { setIsUploading(false); }
|
||||
};
|
||||
|
||||
const formatFileSize = (bytes: number) => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
// ── 结构比对全页面视图 ──
|
||||
if (showComparison) {
|
||||
return (
|
||||
@@ -849,7 +939,7 @@ export default function ReviewDetails() {
|
||||
<button type="button" className="flex items-center gap-1 text-slate-600 hover:text-slate-900 text-[12.5px]" onClick={() => setShowComparison(false)}>
|
||||
<i className="ri-arrow-left-line" /> 返回
|
||||
</button>
|
||||
<span className="font-medium text-sm text-slate-800 truncate">{effectiveReviewData?.fileInfo?.fileName}</span>
|
||||
<span className="font-medium text-sm text-slate-800 truncate">{reviewData?.fileInfo?.fileName}</span>
|
||||
</header>
|
||||
<div className="flex-1 min-h-0 overflow-auto">
|
||||
<Comparison comparison_document={comparison_document} />
|
||||
@@ -858,6 +948,44 @@ export default function ReviewDetails() {
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoaderError) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-slate-50 px-6">
|
||||
<div className="w-full max-w-xl rounded-xl border border-slate-200 bg-white shadow-sm p-8">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-amber-50 text-amber-600 flex items-center justify-center shrink-0">
|
||||
<i className="ri-error-warning-line text-xl" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h1 className="text-lg font-semibold text-slate-900">评查详情暂时无法打开</h1>
|
||||
<p className="mt-2 text-sm text-slate-600 leading-6 break-words">
|
||||
{loaderData.message || '文档不存在、当前账号无权限访问,或评查数据尚未准备完成。'}
|
||||
</p>
|
||||
<div className="mt-5 flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2 rounded-md bg-[#00684a] px-4 py-2 text-sm font-medium text-white hover:bg-[#00543c]"
|
||||
onClick={() => navigate(getReturnUrl())}
|
||||
>
|
||||
<i className="ri-arrow-left-line" />
|
||||
返回列表
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2 rounded-md border border-slate-200 px-4 py-2 text-sm text-slate-600 hover:bg-slate-50"
|
||||
onClick={() => window.location.reload()}
|
||||
>
|
||||
<i className="ri-refresh-line" />
|
||||
重新加载
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen overflow-hidden">
|
||||
{isLoading ? (
|
||||
@@ -865,14 +993,14 @@ export default function ReviewDetails() {
|
||||
<div className="loading-spinner"></div>
|
||||
<span className="ml-3">加载中...</span>
|
||||
</div>
|
||||
) : effectiveReviewData ? (
|
||||
) : reviewData ? (
|
||||
<main className="flex-1 min-h-0 grid grid-cols-[22%,1fr,30%] p-2">
|
||||
{/* 左栏:规则目录 */}
|
||||
<RulesDirectory
|
||||
reviewPoints={effectiveReviewData.reviewPoints}
|
||||
statistics={effectiveReviewData.statistics}
|
||||
reviewPoints={reviewData.reviewPoints}
|
||||
statistics={reviewData.statistics}
|
||||
activeReviewPointResultId={activeReviewPointResultId}
|
||||
fileName={effectiveReviewData.fileInfo.fileName}
|
||||
fileName={reviewData.fileInfo.fileName}
|
||||
onRuleSelect={handleRuleSelect}
|
||||
onBack={() => navigate(getReturnUrl())}
|
||||
/>
|
||||
@@ -886,18 +1014,19 @@ export default function ReviewDetails() {
|
||||
targetPage={targetPage}
|
||||
charPositions={charPositions}
|
||||
activeReviewPointResultId={activeReviewPointResultId}
|
||||
reviewPoints={effectiveReviewData.reviewPoints}
|
||||
reviewPoints={reviewData.reviewPoints}
|
||||
highlightValue={highlightValue}
|
||||
aiSuggestionReplace={aiSuggestionReplace}
|
||||
userInfo={(normalizedLoaderData as any)?.userInfo}
|
||||
userInfo={currentUserInfo || undefined}
|
||||
/>
|
||||
) : (
|
||||
<PdfPreviewTest
|
||||
filePath={previewPath}
|
||||
targetPage={targetPage}
|
||||
charPositions={charPositions}
|
||||
bboxHighlight={bboxHighlight}
|
||||
activeReviewPointResultId={activeReviewPointResultId}
|
||||
reviewPoints={effectiveReviewData.reviewPoints}
|
||||
reviewPoints={reviewData.reviewPoints}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
@@ -907,15 +1036,16 @@ export default function ReviewDetails() {
|
||||
activeTab={rightActiveTab}
|
||||
onTabChange={handleTabChange}
|
||||
activeReviewPoint={activeReviewPoint}
|
||||
reviewPoints={effectiveReviewData.reviewPoints}
|
||||
fileInfo={effectiveReviewData.fileInfo}
|
||||
reviewInfo={effectiveReviewData.reviewInfo}
|
||||
reviewPoints={reviewData.reviewPoints}
|
||||
detailMode={detailMode}
|
||||
fileInfo={reviewData.fileInfo}
|
||||
reviewInfo={reviewData.reviewInfo}
|
||||
onReviewPointSelect={handleReviewPointSelect}
|
||||
onStatusChange={handleReviewPointStatusChange}
|
||||
onConfirmResults={handleConfirmResults}
|
||||
onDownload={handleDownloadFile}
|
||||
auditStatus={document?.auditStatus}
|
||||
fileFormat={effectiveReviewData.fileInfo.fileFormat}
|
||||
fileFormat={reviewData.fileInfo.fileFormat}
|
||||
onUploadTemplate={handleOpenReuploadModal}
|
||||
onComparison={() => setShowComparison(true)}
|
||||
showComparisonButton={showComparisonButton}
|
||||
@@ -939,7 +1069,7 @@ export default function ReviewDetails() {
|
||||
</div>
|
||||
<div className="flex-1 min-h-0">
|
||||
<ComparePreview
|
||||
doc1Path={document?.path || ''}
|
||||
doc1Path={previewPath}
|
||||
doc2Path={comparison_document?.template_contract_path || ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user