diff --git a/app/components/reviews/FilePreview.tsx b/app/components/reviews/FilePreview.tsx index 06bb06c..6f7de00 100644 --- a/app/components/reviews/FilePreview.tsx +++ b/app/components/reviews/FilePreview.tsx @@ -3,45 +3,19 @@ * 显示文档内容和评查点高亮 */ import { useState, useEffect, useRef, ChangeEvent } from 'react'; -import { Document, Page, pdfjs } from 'react-pdf'; +import { pdfjs } from 'react-pdf'; import { DOCUMENT_URL } from '~/api/axios-client'; import { CollaboraViewer, type CollaboraViewerHandle } from '~/components/collabora/CollaboraViewer'; import { requestPageInfo, customGotoPage } from '~/components/collabora/lib'; - -// 导入react-pdf的CSS样式(文本层和注释层必需) -import 'react-pdf/dist/esm/Page/TextLayer.css'; -import 'react-pdf/dist/esm/Page/AnnotationLayer.css'; +import { PdfPreview } from './previewComponents/PdfPreview'; // 设置worker路径为public目录下的worker文件 -// 使用已经下载的兼容版本 (pdfjs-dist v2.12.313) -// 2025/09/28 使用新版本的pdfjs-dist v4.8.69 pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.js'; // 导入统一的ReviewPoint类型 import { type ReviewPoint } from './'; import { toastService } from '../ui/Toast'; -/** - * 自定义样式 - * 这些样式解决了PDF页面在放大时互相重叠的问题 - */ -const styles = { - pdfContainer: { - display: 'flex', - flexDirection: 'column' as const, - alignItems: 'center', - width: '100%', - position: 'relative' as const, - }, - pageContainer: { - display: 'flex', - flexDirection: 'column' as const, - alignItems: 'center', - width: '100%', - position: 'relative' as const, - } -}; - // 定义文档内容类型 interface FileContent { title: string; @@ -78,6 +52,7 @@ interface FilePreviewProps { reviewPoints?: ReviewPoint[]; // 设为可选 activeReviewPointResultId: string | null; targetPage?: number; // 新增目标页码参数 + charPositions?: Array<{ box: number[][], char: string, score: number }>; // 字符位置信息 isStructuredView?: boolean; // 是否显示结构化视图 userInfo?: { sub: string; @@ -86,21 +61,36 @@ interface FilePreviewProps { } // export function FilePreview({ fileContent, reviewPoints, activeReviewPointResultId, targetPage }: FilePreviewProps) { -export function FilePreview({ fileContent, activeReviewPointResultId, targetPage, isStructuredView = false, userInfo }: FilePreviewProps) { - const [zoomLevel, setZoomLevel] = useState(100); - // const [highlightsVisible, setHighlightsVisible] = useState(true); - const contentRef = useRef(null); - const collaboraViewerRef = useRef(null); - const [numPages, setNumPages] = useState(null); - const [loadError, setLoadError] = useState(null); - const [pageInputValue, setPageInputValue] = useState(''); - +export function FilePreview({ fileContent, activeReviewPointResultId, targetPage, charPositions, isStructuredView = false, userInfo }: FilePreviewProps) { // 获取文件类型 const real_path = fileContent.path || fileContent.template_contract_path || ''; const fileExtension = real_path.split('.').pop()?.toLowerCase(); const isDocx = fileExtension === 'docx'; const isPdf = fileExtension === 'pdf'; + // 如果是PDF文件,直接使用PdfPreview组件 + if (isPdf && real_path) { + // console.log('fileContent', fileContent) + // console.log('activeReviewPointResultId', activeReviewPointResultId) + const pageOffset = fileContent.ocrResult?.__meta?.page_offset || 0; + return ( + + ); + } + + // DOCX 和其他文件类型继续使用原有逻辑 + const contentRef = useRef(null); + const collaboraViewerRef = useRef(null); + const [numPages, setNumPages] = useState(null); + const [pageInputValue, setPageInputValue] = useState(''); + // DOCX 页数获取: 使用 requestPageInfo 方法 useEffect(() => { if (!isDocx) return; @@ -144,44 +134,28 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage }; }, [isDocx]); - // 拖拽状态管理 - const [dragMode, setDragMode] = useState(false); // 是否处于拖拽模式 + // 拖拽状态管理(仅用于 DOCX) + const [dragMode, setDragMode] = useState(false); const [isDragging, setIsDragging] = useState(false); const [dragCursor, setDragCursor] = useState('default'); const lastMousePosRef = useRef({ x: 0, y: 0 }); - - // 放大文档 + + // 放大文档(仅用于 DOCX) const handleZoomIn = () => { - if (isDocx) { - // DOCX 文件:调用 Collabora UNO 命令 - if (!collaboraViewerRef.current?.isReady) { - toastService.warning('文档尚未加载完成,请稍候...'); - return; - } - collaboraViewerRef.current?.unoCommands.zoomIn(); - } else if (isPdf) { - // PDF 文件:修改 zoomLevel 状态 - if (zoomLevel < 200) { - setZoomLevel(prevZoom => prevZoom + 10); - } + if (!collaboraViewerRef.current?.isReady) { + toastService.warning('文档尚未加载完成,请稍候...'); + return; } + collaboraViewerRef.current?.unoCommands.zoomIn(); }; - // 缩小文档 + // 缩小文档(仅用于 DOCX) const handleZoomOut = () => { - if (isDocx) { - // DOCX 文件:调用 Collabora UNO 命令 - if (!collaboraViewerRef.current?.isReady) { - toastService.warning('文档尚未加载完成,请稍候...'); - return; - } - collaboraViewerRef.current?.unoCommands.zoomOut(); - } else if (isPdf) { - // PDF 文件:修改 zoomLevel 状态 - if (zoomLevel > 50) { - setZoomLevel(prevZoom => prevZoom - 10); - } + if (!collaboraViewerRef.current?.isReady) { + toastService.warning('文档尚未加载完成,请稍候...'); + return; } + collaboraViewerRef.current?.unoCommands.zoomOut(); }; // 切换拖拽模式 @@ -316,48 +290,29 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage setPageInputValue(value); }; - // 处理页码跳转 + // 处理页码跳转(仅用于 DOCX) const handlePageJump = async () => { if (!pageInputValue) return; const targetPageNum = parseInt(pageInputValue, 10); + const iframeWindow = collaboraViewerRef.current?.getIframeWindow?.(); - if (isDocx) { - // DOCX 文件:调用自定义页面跳转 - const iframeWindow = collaboraViewerRef.current?.getIframeWindow?.(); - if (!iframeWindow) { - toastService.warning('文档尚未加载完成,请稍候...'); - return; - } + if (!iframeWindow) { + toastService.warning('文档尚未加载完成,请稍候...'); + return; + } - if (targetPageNum > 0) { - try { - await customGotoPage(iframeWindow, targetPageNum); - // 跳转成功,清空输入框 - setPageInputValue(''); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : '未知错误'; - toastService.error(`跳转失败: ${errorMessage}`); - } - } else { - toastService.warning('请输入有效页码'); - setPageInputValue(''); - } - } else if (isPdf) { - // PDF 文件:验证页码并滚动到目标页面 - if (!numPages) return; - - if (targetPageNum > 0 && targetPageNum <= numPages) { - // 找到目标页面元素并滚动到该位置 - const pageElement = document.getElementById(`page-${targetPageNum}`); - if (pageElement) { - pageElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); - } - } else { - // 页码超出范围,显示错误信息或重置输入 - toastService.warning(`请输入有效页码 (1-${numPages})`); + if (targetPageNum > 0) { + try { + await customGotoPage(iframeWindow, targetPageNum); setPageInputValue(''); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : '未知错误'; + toastService.error(`跳转失败: ${errorMessage}`); } + } else { + toastService.warning('请输入有效页码'); + setPageInputValue(''); } }; @@ -367,130 +322,14 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage handlePageJump(); } }; - - // PDF文档加载成功回调函数 - function onDocumentLoadSuccess({ numPages }: { numPages: number }) { - setNumPages(numPages); - // console.log("PDF加载成功,页数:", numPages); - } - - // 计算页面在缩放后的实际间距 - const calculatePageMargin = (zoomFactor: number) => { - // 基础间距为30px,随着缩放倍数线性增加 - const baseMargin = 30; - // 页面缩放后,需要额外添加的间距 = (缩放倍数 - 1) * 页面高度 - const additionalMargin = Math.max(0, (zoomFactor - 1) * 800); // 800是估计的页面高度 - return baseMargin + additionalMargin; - }; - // 滚动到顶部 + // 滚动到顶部(仅用于 DOCX) const handleScrollToTop = () => { - if (isDocx) { - // DOCX 文件:调用 Collabora UNO 命令 - if (!collaboraViewerRef.current?.isReady) { - toastService.warning('文档尚未加载完成,请稍候...'); - return; - } - collaboraViewerRef.current?.unoCommands.scrollToTop(); - } else { - // PDF 文件:滚动容器到顶部 - if (contentRef.current) { - contentRef.current.scrollTo({ top: 0, behavior: 'smooth' }); - } + if (!collaboraViewerRef.current?.isReady) { + toastService.warning('文档尚未加载完成,请稍候...'); + return; } - }; - - /** - * 渲染PDF文档的所有页面 - * - * 功能描述: - * 1. 生成PDF所有页面的渲染数组,每个页面包含页码标识和实际页面内容 - * 2. 处理页面缩放,通过CSS transform实现页面大小调整 - * 3. 在每个页面上标记对应的评查点高亮区域 - * 4. 处理评查点的激活状态,显示特殊的高亮效果 - * - * @returns {JSX.Element[] | null} 返回所有页面组件的数组,如果没有页数信息则返回null - */ - const renderAllPages = () => { - // 如果还没有获取到PDF总页数,返回null - if (!numPages) return null; - - // 用于存储所有页面组件的数组 - const pages = []; - - // 遍历每一页,生成对应的页面组件 - for (let i = 1; i <= numPages; i++) { - // 计算当前缩放级别下的页面容器样式 - const zoomFactor = zoomLevel / 100; - const pageContainerStyle = { - ...styles.pageContainer, - marginBottom: `${calculatePageMargin(zoomFactor)}px`, // 动态计算页面间距 - }; - - // 为结构化视图和普通视图创建不同的ID - const pageId = isStructuredView ? `page-${i}-structured` : `page-${i}`; - - // 为每一页创建组件 - pages.push( -
- {/* 页码标识,显示在页面上方 */} -
第 {i} 页
- - {/* 页面容器,应用缩放变换,设置相对定位用于放置评查点高亮 */} -
- {/* 渲染PDF页面组件 */} - - - {/* 渲染评查点高亮区域 */} - {/* {highlightsVisible && pageReviewPoints.map(point => { - // 判断当前评查点是否为激活状态(被选中) - const isActive = point.id === activeReviewPointId; - - return ( - // 评查点高亮区域 -
- ); - })} */} -
-
- ); - } - - // 返回所有页面组件数组 - return pages; + collaboraViewerRef.current?.unoCommands.scrollToTop(); }; // 渲染文档内容 @@ -511,68 +350,9 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage ); } - // console.log('real_path',real_path); - - // PDF内容渲染 - const renderPdfContent = () => ( -
100 ? `${zoomLevel}%` : '100%', - width: '100%', - overflow: 'visible' - }} - > - { - console.error("PDF加载错误:", error); - setLoadError("PDF文档加载失败:" + (error.message || "未知错误")); - }} - className="w-full" - error={
PDF文档加载失败,请检查链接或网络连接。
} - noData={
无数据
} - loading={
PDF加载中...
} - > - {renderAllPages()} -
-
- ); - - // 结构化数据渲染 - const renderStructuredData = () => ( -
-
结构化数据:
- {fileContent.ocrResult ? ( -
-
-              {JSON.stringify(fileContent.ocrResult, null, 2)}
-            
-
- ) : ( -
-

无结构化数据可显示

-
- )} -
- ); - // 根据文件类型选择不同的渲染方式 - if (fileExtension === 'pdf') { - // 结构化视图模式:显示PDF和结构化数据 - if (isStructuredView) { - return ( -
- {renderPdfContent()} - {renderStructuredData()} -
- ); - } - // 普通模式:仅显示PDF - return renderPdfContent(); - } else if (fileExtension === 'docx') { + // 注意:PDF 文件已在组件开头使用 PdfPreview 组件提前返回 + if (fileExtension === 'docx') { // DOCX文件使用Collabora Online预览 return ( )}
- - 比例:{zoomLevel}% - diff --git a/app/components/reviews/ReviewPointsList.tsx b/app/components/reviews/ReviewPointsList.tsx index 4cc7207..8f9813c 100644 --- a/app/components/reviews/ReviewPointsList.tsx +++ b/app/components/reviews/ReviewPointsList.tsx @@ -68,6 +68,16 @@ const getRuleTypeText = (type?: string): string => { return ruleTypeMap[type] || type; }; +/** + * 字符位置类型定义 + * 用于定位文档中具体的文字位置 + */ +export interface CharPosition { + box: number[][]; // 字符边界框坐标 + char: string; // 字符内容 + score: number; // OCR识别置信度 +} + /** * 评查点类型定义 * 用于展示单个评查结果 @@ -136,7 +146,7 @@ interface ReviewPointsListProps { reviewPoints: ReviewPoint[]; statistics: Statistics; activeReviewPointResultId: string | null; - onReviewPointSelect: (id: string, page?: number) => void; + onReviewPointSelect: (id: string, page?: number, charPositions?: CharPosition[]) => void; onStatusChange: (id: string, editAuditStatusId: string | number, status: string, message: string) => void; } @@ -816,15 +826,15 @@ export function ReviewPointsList({ // console.log('singleReviewPoint-------', singleReviewPoint); // 检查是否存在配置和pairs数组 - const config = singleReviewPoint.config as { - logic?: string; - pairs?: Array<{ - sourceField: Record; - targetField: Record; + const config = singleReviewPoint.config as { + logic?: string; + pairs?: Array<{ + sourceField: Record; + targetField: Record; res: boolean; compareMethod?: string; - }>; - selectedFields?: string[] + }>; + selectedFields?: string[] } | undefined; if (!config || !config.pairs || !Array.isArray(config.pairs) || config.pairs.length === 0) { @@ -865,27 +875,28 @@ export function ReviewPointsList({ // 查找链条关系 const findChains = () => { - type ChainItem = { - field: string; - data: { - key: string; - page: number; - value: string - }; + type ChainItem = { + field: string; + data: { + key: string; + page: number; + value: string; + char_positions?: CharPosition[]; + }; res: boolean; - compareMethod?: string; + compareMethod?: string; }; const chains: Array> = []; const visited = new Set(); // 构建字段映射关系 - const fieldMap = new Map>(); @@ -1102,7 +1113,7 @@ export function ReviewPointsList({ for (const item of chain) { if (item.data.page && typeof onReviewPointSelect === 'function') { hasPage = true; - onReviewPointSelect(reviewPoint.id, Number(item.data.page)); + onReviewPointSelect(reviewPoint.id, Number(item.data.page), item.data.char_positions); break; } } @@ -1116,7 +1127,7 @@ export function ReviewPointsList({ // 遍历chain找到第一个有效的page for (const item of chain) { if (item.data.page && typeof onReviewPointSelect === 'function') { - onReviewPointSelect(reviewPoint.id, Number(item.data.page)); + onReviewPointSelect(reviewPoint.id, Number(item.data.page), item.data.char_positions); break; } } @@ -1152,11 +1163,11 @@ export function ReviewPointsList({ onClick={(e) => { e.stopPropagation(); if (item.data.page) { - // console.log('currentitem-------', reviewPoint); + console.log('点击了长链条评查点', item.data.char_positions); // 假设onReviewPointSelect在作用域内可用 const reviewPointId = reviewPoint.id as string; if (reviewPointId && typeof onReviewPointSelect === 'function') { - onReviewPointSelect(reviewPointId, Number(item.data.page)); + onReviewPointSelect(reviewPointId, Number(item.data.page), item.data.char_positions); } } else if(reviewPoint.contentPage && reviewPoint.contentPage[item.field]){ @@ -1237,9 +1248,10 @@ export function ReviewPointsList({ onClick={(e) => { e.stopPropagation(); if (chain[0].data.page) { + console.log('点击了短链1左', chain[0].data.char_positions) const reviewPointId = reviewPoint.id as string; if (reviewPointId && typeof onReviewPointSelect === 'function') { - onReviewPointSelect(reviewPointId, chain[0].data.page); + onReviewPointSelect(reviewPointId, chain[0].data.page, chain[0].data.char_positions); } } else if(reviewPoint.contentPage && reviewPoint.contentPage[chain[0].field]){ @@ -1263,9 +1275,10 @@ export function ReviewPointsList({ onClick={(e) => { e.stopPropagation(); if (chain[1].data.page) { + console.log('点击了短链2右', chain[1].data.char_positions) const reviewPointId = reviewPoint.id as string; if (reviewPointId && typeof onReviewPointSelect === 'function') { - onReviewPointSelect(reviewPointId, chain[1].data.page); + onReviewPointSelect(reviewPointId, chain[1].data.page, chain[1].data.char_positions); } } else if(reviewPoint.contentPage && reviewPoint.contentPage[chain[1].field]){ @@ -1338,12 +1351,13 @@ export function ReviewPointsList({ */ const renderOtherRule = (otherRule: Record, reviewPoint: ReviewPoint) => { const fieldKey = otherRule.fieldKey as string; - const fieldValue = otherRule.fieldValue as { - type: Record; + char_positions?: CharPosition[]; + }>; }; // 获取res的综合结果 @@ -1404,7 +1418,8 @@ export function ReviewPointsList({ onClick={(e) => { e.stopPropagation(); if (mainTypeValue.page && typeof onReviewPointSelect === 'function') { - onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page)); + console.log("点击了其他评查点", mainTypeValue) + onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), mainTypeValue.char_positions); }else if(reviewPoint.contentPage && reviewPoint.contentPage[fieldKey]){ onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[fieldKey])); }else{ @@ -1415,7 +1430,7 @@ export function ReviewPointsList({ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if (mainTypeValue.page && typeof onReviewPointSelect === 'function') { - onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page)); + onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), mainTypeValue.char_positions); }else{ toastService.error(`没有找到${fieldKey}对应的索引内容`); } @@ -1478,12 +1493,13 @@ export function ReviewPointsList({ const renderModelRule = (aiRule: Record, reviewPoint: ReviewPoint) => { // 从aiRule中提取配置信息 - const config = aiRule.config as { - model?: string; - fields?: Record; + char_positions?: CharPosition[]; + }>; message?: string; res?: boolean; } | undefined; @@ -1525,7 +1541,8 @@ export function ReviewPointsList({ onClick={(e) => { e.stopPropagation(); if (value.page && typeof onReviewPointSelect === 'function') { - onReviewPointSelect(reviewPoint.id, Number(value.page)); + console.log("点击了大模型的评查点", value.char_positions) + onReviewPointSelect(reviewPoint.id, Number(value.page), value.char_positions); }else if(reviewPoint.contentPage && reviewPoint.contentPage[key]){ onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key])); }else{ @@ -1537,7 +1554,7 @@ export function ReviewPointsList({ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if (value.page && typeof onReviewPointSelect === 'function') { - onReviewPointSelect(reviewPoint.id, Number(value.page)); + onReviewPointSelect(reviewPoint.id, Number(value.page), value.char_positions); }else if(reviewPoint.contentPage && reviewPoint.contentPage[key]){ onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key])); }else{ @@ -1652,6 +1669,7 @@ export function ReviewPointsList({ interface RuleFieldValue { page?: number | string; value?: string; + char_positions?: CharPosition[]; type: Record; } @@ -1670,7 +1688,7 @@ export function ReviewPointsList({ // 使用类型断言获取config对象的具体结构 const config = rule.config as { res: boolean; - fields: Record; + fields: Record; logic?: string; }; @@ -1717,7 +1735,7 @@ export function ReviewPointsList({ // 使用类型断言获取config对象的具体结构 const config = rule.config as { res: boolean; - field: Record; + field: Record; formatType?: string; parameters?: string; }; @@ -1751,7 +1769,7 @@ export function ReviewPointsList({ logic: string; res: boolean; conditions: Array<{ - field: Record; + field: Record; value: string; operator: string; res: boolean; @@ -1786,7 +1804,7 @@ export function ReviewPointsList({ // 使用类型断言获取config对象的具体结构 const config = rule.config as { res: boolean; - field: Record; + field: Record; pattern?: string; matchType?: string; selectedFields?: string[]; @@ -1821,10 +1839,11 @@ export function ReviewPointsList({ res: boolean; page?: number | string; value?: string; + char_positions?: CharPosition[]; }>; }; }> = []; - + // 使用对象存储相同fieldKey的项,便于快速查找和合并 const fieldKeyMap: Record; }; }> = {}; @@ -1843,11 +1863,12 @@ export function ReviewPointsList({ const fieldValue = item.fieldValue; const typeKey = Object.keys(fieldValue.type)[0]; // 获取类型名称(exists/logic/regex/format) const typeValue = fieldValue.type[typeKey]; // 获取类型值(true/false) - - // 提取页码和值 + + // 提取页码、值和字符位置 const page = fieldValue.page; const value = fieldValue.value; - + const char_positions = fieldValue.char_positions; + // 如果是第一次遇到这个fieldKey,创建新条目 if (!fieldKeyMap[fieldKey]) { // 创建新的结构 @@ -1858,12 +1879,13 @@ export function ReviewPointsList({ } }; } - + // 将类型信息添加到type对象中,允许一个字段有多种规则类型的结果 fieldKeyMap[fieldKey].fieldValue.type[typeKey] = { res: typeValue, page, - value + value, + char_positions }; }); @@ -2270,7 +2292,7 @@ export function ReviewPointsList({ tabIndex={0} style={{ userSelect: 'text' }} onClick={() => { - // console.log('reviewPoint', reviewPoint); + console.log('reviewPoint', reviewPoint); handleReviewPointClick(reviewPoint.id); }} onKeyDown={(e) => { diff --git a/app/routes/reviews.tsx b/app/routes/reviews.tsx index d8d39f4..e511678 100644 --- a/app/routes/reviews.tsx +++ b/app/routes/reviews.tsx @@ -300,6 +300,7 @@ export default function ReviewDetails() { const [activeReviewPointResultId, setActiveReviewPointResultId] = useState(null); const [targetPage, setTargetPage] = useState(undefined); const [templateTargetPage, setTemplateTargetPage] = useState(undefined); + const [charPositions, setCharPositions] = useState | undefined>(undefined); const [pendingUpdate, setPendingUpdate] = useState<{ reviewPointResultId: string; newStatus: string; @@ -367,19 +368,22 @@ export default function ReviewDetails() { setActiveTab(tabKey); }; - const handleReviewPointSelect = (reviewPointId: string, page?: number) => { + const handleReviewPointSelect = (reviewPointId: string, page?: number, charPos?: Array<{ box: number[][], char: string, score: number }>) => { // 如果点击的是相同的评查点,但有page参数,先重置targetPage以确保useEffect能够触发 if (reviewPointId === activeReviewPointResultId && page) { setTargetPage(undefined); - // 使用setTimeout确保状态更新后再设置新的targetPage + setCharPositions(undefined); + // 使用setTimeout确保状态更新后再设置新的targetPage和charPositions setTimeout(() => { setActiveReviewPointResultId(reviewPointId); setTargetPage(page); + setCharPositions(charPos); }, 0); } else { - // 正常设置activeReviewPointId和targetPage + // 正常设置activeReviewPointId、targetPage和charPositions setActiveReviewPointResultId(reviewPointId); setTargetPage(page); + setCharPositions(charPos); } }; @@ -713,11 +717,12 @@ export default function ReviewDetails() {
{/* 左侧:文件预览 */}
-
@@ -739,11 +744,12 @@ export default function ReviewDetails() {
{/* 左侧:原文件预览 */}
-