From 9b8147294d05f9212c34ed295e27ed5e56176a47 Mon Sep 17 00:00:00 2001 From: yorn <1057707203@qq.com> Date: Mon, 9 Mar 2026 17:54:11 +0800 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20=E6=B7=BB=E5=8A=A0=E6=B9=9B=E6=B1=9F?= =?UTF-8?q?=E7=9A=84=E5=9C=B0=E5=8C=BA=E9=80=89=E6=8B=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routes/entry-modules._index.tsx | 17 +++++++++++++++++ app/routes/entry-modules.new.tsx | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/app/routes/entry-modules._index.tsx b/app/routes/entry-modules._index.tsx index cb695d0..61798ec 100644 --- a/app/routes/entry-modules._index.tsx +++ b/app/routes/entry-modules._index.tsx @@ -100,6 +100,23 @@ const AREA_OPTIONS = [ { value: "云浮", label: "云浮" }, { value: "揭阳", label: "揭阳" }, { value: "潮州", label: "潮州" }, + { value: "湛江", label: "湛江" }, + // { value: "广州", label: "广州" }, + // { value: "深圳", label: "深圳" }, + // { value: "珠海", label: "珠海" }, + // { value: "佛山", label: "佛山" }, + // { value: "惠州", label: "惠州" }, + // { value: "江门", label: "江门" }, + // { value: "茂名", label: "茂名" }, + // { value: "汕尾", label: "汕尾" }, + // { value: "汕头", label: "汕头" }, + // { value: "河源", label: "河源" }, + // { value: "阳江", label: "阳江" }, + // { value: "清远", label: "清远" }, + // { value: "东莞", label: "东莞" }, + // { value: "中山", label: "中山" }, + // { value: "肇庆", label: "肇庆" }, + // { value: "韶关", label: "韶关" }, { value: "省局", label: "省局" } ]; diff --git a/app/routes/entry-modules.new.tsx b/app/routes/entry-modules.new.tsx index e7efb0e..f594ef4 100644 --- a/app/routes/entry-modules.new.tsx +++ b/app/routes/entry-modules.new.tsx @@ -76,6 +76,23 @@ const AREA_OPTIONS = [ { value: "云浮", label: "云浮" }, { value: "揭阳", label: "揭阳" }, { value: "潮州", label: "潮州" }, + { value: "湛江", label: "湛江" }, + // { value: "广州", label: "广州" }, + // { value: "深圳", label: "深圳" }, + // { value: "珠海", label: "珠海" }, + // { value: "佛山", label: "佛山" }, + // { value: "惠州", label: "惠州" }, + // { value: "江门", label: "江门" }, + // { value: "茂名", label: "茂名" }, + // { value: "汕尾", label: "汕尾" }, + // { value: "汕头", label: "汕头" }, + // { value: "河源", label: "河源" }, + // { value: "阳江", label: "阳江" }, + // { value: "清远", label: "清远" }, + // { value: "东莞", label: "东莞" }, + // { value: "中山", label: "中山" }, + // { value: "肇庆", label: "肇庆" }, + // { value: "韶关", label: "韶关" }, { value: "省局", label: "省局" } ]; From 4601716256ce72ab5ef1fd0a2c205303b28b99ba Mon Sep 17 00:00:00 2001 From: yorn <1057707203@qq.com> Date: Fri, 13 Mar 2026 14:10:55 +0800 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=E6=B8=B2=E6=9F=93=E5=A4=A7?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E7=9A=84=E8=AF=84=E6=9F=A5=E7=BB=93=E6=9E=9C?= =?UTF-8?q?=E4=B9=9F=E6=B7=BB=E5=8A=A0=E4=B8=8Atrue=E5=92=8Cfalse=E7=9A=84?= =?UTF-8?q?=E5=88=A4=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/cross-checking/ReviewPointsList.tsx | 9 +++++---- app/components/reviews/ReviewPointsList.tsx | 10 ++++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/app/components/cross-checking/ReviewPointsList.tsx b/app/components/cross-checking/ReviewPointsList.tsx index 023e842..d23e79f 100644 --- a/app/components/cross-checking/ReviewPointsList.tsx +++ b/app/components/cross-checking/ReviewPointsList.tsx @@ -1896,6 +1896,7 @@ export function ReviewPointsList({ fields?: Record; ai_suggestion?: { @@ -1954,8 +1955,8 @@ export function ReviewPointsList({ - )} - { reviewPoint.pointName === '签署甲方详细信息校验' && ( + ); + })()} + { reviewPoint.pointName === '签署甲方详细信息校验' && (() => { + const firstContentKey = reviewPoint.content ? Object.keys(reviewPoint.content)[0] : undefined; + const firstContentValue = firstContentKey ? reviewPoint.content![firstContentKey]?.value : undefined; + return ( - )} + ); + })()} {/*
{reviewPoint.title}
diff --git a/app/components/reviews/ReviewPointsList.tsx b/app/components/reviews/ReviewPointsList.tsx index c71b5db..5a117f0 100644 --- a/app/components/reviews/ReviewPointsList.tsx +++ b/app/components/reviews/ReviewPointsList.tsx @@ -1806,6 +1806,7 @@ export function ReviewPointsList({ if (!isReplaceDisabled && onAiSuggestionReplace && config.fields) { // 从 config.fields[key] 中获取对应的字段信息 const fieldData = config.fields[key]; + console.log("替换原始数据", config, key) if (fieldData) { // 调用回调函数,传递搜索文本(原文)、替换文本(AI建议)和页码 onAiSuggestionReplace( @@ -2502,7 +2503,10 @@ export function ReviewPointsList({ {/*
*/}
{reviewPoint.pointName}
- { reviewPoint.pointName === '签署乙方详细信息校验' && ( + { reviewPoint.pointName === '签署乙方详细信息校验' && (() => { + const firstContentKey = reviewPoint.content ? Object.keys(reviewPoint.content)[0] : undefined; + const firstContentValue = firstContentKey ? reviewPoint.content![firstContentKey]?.value : undefined; + return ( - )} - { reviewPoint.pointName === '签署甲方详细信息校验' && ( + ); + })()} + { reviewPoint.pointName === '签署甲方详细信息校验' && (() => { + const firstContentKey = reviewPoint.content ? Object.keys(reviewPoint.content)[0] : undefined; + const firstContentValue = firstContentKey ? reviewPoint.content![firstContentKey]?.value : undefined; + return ( - )} + ); + })()}
{/*
From cdf1d4f096d7adfd9096e49aac433c1abc59c0ba Mon Sep 17 00:00:00 2001 From: yorn <1057707203@qq.com> Date: Tue, 14 Apr 2026 09:45:12 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=A6=96=E9=A1=B5?= =?UTF-8?q?=E4=BA=A4=E5=8F=89=E8=AF=84=E6=9F=A5=E5=92=8C=E7=9F=A5=E8=AF=86?= =?UTF-8?q?=E5=BA=93=E5=85=A5=E5=8F=A3=E7=9A=84=E6=B8=B2=E6=9F=93=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routes/_index.tsx | 105 ++++++++++++++++++++---------------------- 1 file changed, 51 insertions(+), 54 deletions(-) diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index 330015c..c109476 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -59,6 +59,7 @@ export async function loader({ request }: LoaderFunctionArgs) { if (userRole && frontendJWT) { const { getUserRoutesByRole } = await import('~/api/auth/user-routes'); const routesResult = await getUserRoutesByRole(userRole, frontendJWT, true); // includeHidden=true + // console.log('🔍 [Index Loader] 顶级路由paths:', routesResult.data?.map(r => r.path)); if (routesResult.success && routesResult.data) { // 查找 '/settings' 路由及其子路由 @@ -439,7 +440,6 @@ export default function Index() { loaderData.entryModules && loaderData.entryModules.length > 0 ? ( <> {loaderData.entryModules.map((module) => { - // 判断是否为智慧法务助手,如果是且有交叉评查权限,则在其之前插入交叉评查卡片 const isLLMModule = module.name === '智慧法务助手'; // 🔑 如果是智慧法务助手且用户没有访问权限,则不渲染该模块 @@ -448,61 +448,58 @@ export default function Index() { } return ( - - {/* 在智慧法务助手之前插入交叉评查入口 */} - {isLLMModule && loaderData.hasCrossCheckingAccess && ( -
{ - if (e.key === 'Enter' || e.key === ' ') { - handleEnterCrossChecking(); - } - }} - role="button" - tabIndex={0} - aria-label="交叉评查" - > - 交叉评查 { - // 如果图片加载失败,使用 icon - (e.target as HTMLImageElement).style.display = 'none'; - const parent = (e.target as HTMLImageElement).parentElement; - if (parent) { - const icon = document.createElement('i'); - icon.className = 'ri-shuffle-line'; - icon.style.fontSize = '48px'; - icon.style.color = 'var(--color-primary)'; - parent.insertBefore(icon, parent.firstChild); - } - }} - /> - 交叉评查 -
- )} - - {/* 渲染原有模块 */} -
handleModuleClick(module)} - onKeyDown={(e) => handleKeyDown(module, e)} - role="button" - tabIndex={0} - aria-label={module.name} - > - {module.name} - {module.name} -
-
+
handleModuleClick(module)} + onKeyDown={(e) => handleKeyDown(module, e)} + role="button" + tabIndex={0} + aria-label={module.name} + > + {module.name} + {module.name} +
); })} + + {/* 交叉评查入口 - 独立渲染,不依赖智慧法务助手模块 */} + {loaderData.hasCrossCheckingAccess && ( +
{ + if (e.key === 'Enter' || e.key === ' ') { + handleEnterCrossChecking(); + } + }} + role="button" + tabIndex={0} + aria-label="交叉评查" + > + 交叉评查 { + (e.target as HTMLImageElement).style.display = 'none'; + const parent = (e.target as HTMLImageElement).parentElement; + if (parent) { + const icon = document.createElement('i'); + icon.className = 'ri-shuffle-line'; + icon.style.fontSize = '48px'; + icon.style.color = 'var(--color-primary)'; + parent.insertBefore(icon, parent.firstChild); + } + }} + /> + 交叉评查 +
+ )} ) : (
From 4e19672b386ee8b06ca8c3119ca94b244a51f814 Mon Sep 17 00:00:00 2001 From: yorn <1057707203@qq.com> Date: Fri, 17 Apr 2026 19:01:52 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=E6=B7=BB=E5=8A=A0pdf=E7=9A=84=E7=95=A5?= =?UTF-8?q?=E7=BC=A9=E5=9B=BE=E7=BB=84=E4=BB=B6=E7=9A=84=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/reviews/FilePreview.tsx | 7 +- .../previewComponents/PdfPreviewTest.tsx | 631 ++++++++ app/routes/reviews.tsx | 54 +- app/routes/reviewsTest.tsx | 1257 +++++++++++++++ docs/design/7c-简化C-极简(1).html | 1435 +++++++++++++++++ 5 files changed, 3330 insertions(+), 54 deletions(-) create mode 100644 app/components/reviews/previewComponents/PdfPreviewTest.tsx create mode 100644 app/routes/reviewsTest.tsx create mode 100644 docs/design/7c-简化C-极简(1).html diff --git a/app/components/reviews/FilePreview.tsx b/app/components/reviews/FilePreview.tsx index adbd7d9..97262ce 100644 --- a/app/components/reviews/FilePreview.tsx +++ b/app/components/reviews/FilePreview.tsx @@ -6,6 +6,7 @@ import { useState, useEffect, useRef, forwardRef, useImperativeHandle, ChangeEve import { CollaboraViewer, type CollaboraViewerHandle } from '~/components/collabora/CollaboraViewer'; import { requestPageInfo, customGotoPage } from '~/components/collabora/lib'; import { PdfPreview } from './previewComponents/PdfPreview'; +import { PdfPreviewTest } from './previewComponents/PdfPreviewTest'; import { toastService } from '../ui/Toast'; // 直接从ReviewPointsList导入类型,避免循环依赖 @@ -74,7 +75,7 @@ export interface FilePreviewHandle { } // export function FilePreview({ fileContent, reviewPoints, activeReviewPointResultId, targetPage }: FilePreviewProps) { -export const FilePreview = forwardRef(function FilePreview({ fileContent, activeReviewPointResultId, targetPage, charPositions, highlightValue, isStructuredView = false, userInfo, aiSuggestionReplace, isTemplate = false }, ref) { +export const FilePreview = forwardRef(function FilePreview({ fileContent, reviewPoints, activeReviewPointResultId, targetPage, charPositions, highlightValue, isStructuredView = false, userInfo, aiSuggestionReplace, isTemplate = false }, ref) { // 获取文件类型 const real_path = fileContent.path || fileContent.template_contract_path || ''; const fileExtension = real_path.split('.').pop()?.toLowerCase(); @@ -232,10 +233,12 @@ export const FilePreview = forwardRef(funct const pageOffset = fileContent.ocrResult?.__meta?.page_offset || fileContent.ocr_result?.__meta?.page_offset || 0; // console.log('pageOffset', pageOffset) return ( - ; + isStructuredView?: boolean; + activeReviewPointResultId?: string | null; + pageOffset?: number; + onNumPagesChange?: (numPages: number) => void; + onZoomChange?: (zoomLevel: number) => void; + + // 新增(可选):用于派生略缩图徽标和当前高亮标签 + reviewPoints?: ReviewPoint[]; +} + +// ============================================================ +// ReviewPoint → 状态映射 +// ============================================================ +const STATUS_ORDER: Record = { fail: 0, warn: 1, pending: 2, pass: 3 }; + +const STATUS_BADGE: Record = { + fail: { cls: 'bg-red-500', ic: 'ri-close-circle-fill' }, + warn: { cls: 'bg-amber-500', ic: 'ri-lightbulb-flash-fill' }, + pending: { cls: 'bg-orange-500', ic: 'ri-question-fill' }, + pass: { cls: 'bg-emerald-500', ic: 'ri-checkbox-circle-fill' }, +}; + +function classifyReviewPoint(p: ReviewPoint): WorstStatus | 'skipped' { + const status = p.status; + if (status === 'notApplicable' || status === 'not_applicable') return 'skipped'; + if (p.result === true || (p.result === undefined && status === 'success')) return 'pass'; + if (p.result === false) { + if (status === 'error') return 'fail'; + if (status === 'warning' || status === 'info') return 'warn'; + } + if (status === 'success') return 'pass'; + if (status === 'warning' || status === 'info') return 'warn'; + if (status === 'error') return 'fail'; + return 'pass'; +} + +/** 把 ReviewPoint 涉及的所有页号(字段级)抽出 */ +function getPointPages(p: ReviewPoint): number[] { + const set = new Set(); + const addMaybe = (v: unknown) => { + const n = typeof v === 'string' ? parseInt(v, 10) : typeof v === 'number' ? v : NaN; + if (Number.isFinite(n) && n > 0) set.add(n); + }; + if (p.contentPage) Object.values(p.contentPage).forEach(addMaybe); + if (p.content) Object.values(p.content).forEach(v => addMaybe(v?.page)); + return [...set].sort((a, b) => a - b); +} + +/** 当前规则在某页涉及的字段名列表 */ +function getFieldsOnPage(p: ReviewPoint, page: number): string[] { + const fields: string[] = []; + const matches = (v: unknown) => { + const n = typeof v === 'string' ? parseInt(v, 10) : typeof v === 'number' ? v : NaN; + return n === page; + }; + if (p.contentPage) { + Object.entries(p.contentPage).forEach(([k, v]) => { + if (matches(v)) fields.push(k); + }); + } + if (!fields.length && p.content) { + Object.entries(p.content).forEach(([k, v]) => { + if (matches(v?.page)) fields.push(k); + }); + } + return fields; +} + +// ============================================================ +// 组件 +// ============================================================ +export function PdfPreviewTest({ + filePath, + targetPage, + charPositions, + isStructuredView = false, + activeReviewPointResultId, + pageOffset = 0, + onNumPagesChange, + onZoomChange, + reviewPoints, +}: PdfPreviewProps) { + // ---------- 基础状态 ---------- + const [numPages, setNumPages] = useState(null); + const [currentPage, setCurrentPage] = useState(1); + const [zoomLevel, setZoomLevel] = useState(100); + const [loadError, setLoadError] = useState(null); + + // 略缩图面板 + const [showThumbs, setShowThumbs] = useState(true); + const [thumbMode, setThumbMode] = useState<'filtered' | 'all'>('filtered'); + + // 坐标校准(保留主视口高亮逻辑) + const [coordinateScale, setCoordinateScale] = useState(0.83); + const [isScaleAutoCalculated, setIsScaleAutoCalculated] = useState(false); + + // 页码跳转输入 + const [pageInputValue, setPageInputValue] = useState(''); + + const viewportRef = useRef(null); + const thumbsPanelRef = useRef(null); + + // ---------- 派生数据 ---------- + const fileUrl = useMemo( + () => `/api/pdf-proxy?path=${encodeURIComponent(filePath)}`, + [filePath], + ); + + const activePoint = useMemo( + () => reviewPoints?.find(p => p.id === activeReviewPointResultId), + [reviewPoints, activeReviewPointResultId], + ); + + // 当前规则涉及的页(字段级,带 pageOffset) + const rulePages = useMemo(() => { + if (!activePoint) return []; + return getPointPages(activePoint).map(p => p + pageOffset); + }, [activePoint, pageOffset]); + + // 每页状态聚合 + const pageStatusMap = useMemo>(() => { + const m = new Map(); + if (!reviewPoints?.length) return m; + reviewPoints.forEach(p => { + const cls = classifyReviewPoint(p); + if (cls === 'skipped') return; + const pages = getPointPages(p).map(x => x + pageOffset); + pages.forEach(pg => { + const cur = m.get(pg) || { worst: 'pass', count: 0, issues: 0 }; + if (STATUS_ORDER[cls] < STATUS_ORDER[cur.worst]) cur.worst = cls; + cur.count += 1; + if (cls !== 'pass') cur.issues += 1; + m.set(pg, cur); + }); + }); + return m; + }, [reviewPoints, pageOffset]); + + // 当前高亮标签(工具栏右侧) + const highlightLabel = useMemo(() => { + if (!activePoint) return null; + const code = activePoint.pointCode || activePoint.id; + const name = activePoint.pointName || activePoint.title || ''; + return `${code}${name ? ' · ' + name : ''}`; + }, [activePoint]); + + // ---------- 通知上层 ---------- + useEffect(() => { + if (numPages && onNumPagesChange) onNumPagesChange(numPages); + }, [numPages, onNumPagesChange]); + + useEffect(() => { + if (onZoomChange) onZoomChange(zoomLevel); + }, [zoomLevel, onZoomChange]); + + // ---------- targetPage 跳转 ---------- + useEffect(() => { + if (targetPage && numPages) { + const next = Math.max(1, Math.min(numPages, targetPage + pageOffset)); + setCurrentPage(next); + } + }, [targetPage, numPages, pageOffset, activeReviewPointResultId]); + + // ---------- 切换规则:重置略缩图模式为 filtered ---------- + useEffect(() => { + if (activeReviewPointResultId) setThumbMode('filtered'); + }, [activeReviewPointResultId]); + + // ---------- 文件路径变化:重置坐标自动计算 ---------- + useEffect(() => { + setIsScaleAutoCalculated(false); + }, [filePath]); + + // ---------- 略缩图滚动到当前页 ---------- + useEffect(() => { + const host = thumbsPanelRef.current; + if (!host) return; + const el = host.querySelector(`[data-thumb-page="${currentPage}"]`); + if (el) el.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); + }, [currentPage, thumbMode, showThumbs]); + + // ---------- PDF 加载 ---------- + const onDocumentLoadSuccess = useCallback( + ({ numPages: n }: { numPages: number }) => { + setNumPages(n); + // 初始页:优先 targetPage,否则第 1 页 + if (targetPage) { + setCurrentPage(Math.max(1, Math.min(n, targetPage + pageOffset))); + } else { + setCurrentPage(p => Math.max(1, Math.min(n, p))); + } + }, + [targetPage, pageOffset], + ); + + const onDocumentLoadError = useCallback((error: Error) => { + console.error('PDF加载错误:', error); + setLoadError('PDF文档加载失败:' + (error.message || '未知错误')); + }, []); + + // ---------- 主页面加载(自动校准坐标) ---------- + const onMainPageLoadSuccess = useCallback( + (page: any) => { + if (isScaleAutoCalculated) return; + setTimeout(() => { + const pdfOriginalWidthPt = page.view?.[2] || page.originalWidth || page.width; + const canvas = viewportRef.current?.querySelector( + '.pdf-main-canvas .react-pdf__Page__canvas', + ) as HTMLCanvasElement | null; + if (canvas && pdfOriginalWidthPt) { + const canvasDisplayWidth = canvas.offsetWidth; + const currentScale = zoomLevel / 100; + const autoScale = (canvasDisplayWidth / currentScale) / pdfOriginalWidthPt; + setCoordinateScale(autoScale); + setIsScaleAutoCalculated(true); + } + }, 200); + }, + [isScaleAutoCalculated, zoomLevel], + ); + + // ---------- 翻页 / 缩放 ---------- + const goPrev = () => setCurrentPage(p => Math.max(1, p - 1)); + const goNext = () => setCurrentPage(p => Math.min(numPages || p, p + 1)); + const zoomIn = () => setZoomLevel(z => Math.min(200, z + 10)); + const zoomOut = () => setZoomLevel(z => Math.max(50, z - 10)); + + const jumpToHighlight = () => { + if (!activePoint || rulePages.length === 0) { + toastService.info('当前规则无关联页'); + return; + } + const first = rulePages[0]; + if (numPages && first >= 1 && first <= numPages) setCurrentPage(first); + }; + + const handlePageInputChange = (e: React.ChangeEvent) => { + setPageInputValue(e.target.value.replace(/\D/g, '')); + }; + const handlePageJump = () => { + if (!pageInputValue || !numPages) return; + const n = parseInt(pageInputValue, 10); + if (n > 0 && n <= numPages) { + setCurrentPage(n); + setPageInputValue(''); + } else { + toastService.warning(`请输入有效页码 (1-${numPages})`); + setPageInputValue(''); + } + }; + + // ---------- 高亮矩形(对齐原 PdfPreview 的字符位置) ---------- + const mainHighlight = useMemo(() => { + if (!charPositions?.length || !targetPage) { + console.log('[PdfPreviewTest] highlight skipped: no charPositions/targetPage', { + hasCharPositions: !!charPositions?.length, + targetPage, + }); + return null; + } + if (currentPage !== targetPage + pageOffset) { + console.log('[PdfPreviewTest] highlight skipped: page mismatch', { + currentPage, + targetPage, + pageOffset, + expected: targetPage + pageOffset, + }); + return null; + } + + const scale = zoomLevel / 100; + let minX = Infinity, + minY = Infinity, + maxX = -Infinity, + maxY = -Infinity; + const chars: string[] = []; + charPositions.forEach(cp => { + chars.push(cp.char); + cp.box.forEach(pt => { + const [x, y] = pt; + if (x < minX) minX = x; + if (x > maxX) maxX = x; + if (y < minY) minY = y; + if (y > maxY) maxY = y; + }); + }); + return { + x: minX * coordinateScale * scale, + y: minY * coordinateScale * scale, + width: (maxX - minX) * coordinateScale * scale, + height: (maxY - minY) * coordinateScale * scale, + text: chars.join(''), + }; + }, [charPositions, targetPage, currentPage, pageOffset, zoomLevel, coordinateScale]); + + // ---------- 略缩图可见页列表 ---------- + const effThumbMode: 'filtered' | 'all' = + thumbMode === 'filtered' && rulePages.length > 0 ? 'filtered' : 'all'; + + const thumbPages = useMemo(() => { + if (!numPages) return []; + if (effThumbMode === 'filtered') return rulePages.filter(p => p >= 1 && p <= numPages); + return Array.from({ length: numPages }, (_, i) => i + 1); + }, [effThumbMode, rulePages, numPages]); + + // ============================================================ + // 渲染 + // ============================================================ + if (loadError) { + return ( +
{loadError}
+ ); + } + + return ( + PDF 加载中…
} + error={
PDF 文档加载失败
} + noData={
无数据
} + > +
+ {/* ═════ 顶部工具栏 ═════ */} +
+
+ + | + + + e.currentTarget.select()} + onBlur={handlePageJump} + onKeyDown={e => { + if (e.key === 'Enter') handlePageJump(); + }} + className="w-9 h-6 text-center bg-slate-50 border border-slate-200 rounded text-[12px] font-mono outline-none focus:border-primary" + /> + / {numPages ?? '-'} + + + | + + {zoomLevel}% + +
+ +
+ {highlightLabel && ( + <> + 当前高亮: + + + {highlightLabel} + + + + )} +
+
+ + {/* ═════ 主体:略缩图 + 视口 ═════ */} +
+ {/* ── 略缩图面板 ── */} + {showThumbs && ( +
+ {/* 模式切换条 */} +
+
+ + +
+
+ + {/* 略缩图列表 */} +
+ {numPages === null ? ( +
加载中…
+ ) : thumbPages.length === 0 ? ( +
+ +
此规则无关联页面
+
+ ) : ( + thumbPages.map(p => { + const info = pageStatusMap.get(p); + const isCur = p === currentPage; + const isRulePage = rulePages.includes(p); + + let badge: React.ReactNode = null; + if (info) { + const b = STATUS_BADGE[info.worst]; + const num = info.issues > 0 ? info.issues : info.worst === 'pass' ? '' : info.count; + badge = ( + + {num ? num : } + + ); + } + + const frameCls = isCur + ? 'ring-2 ring-primary shadow-md' + : effThumbMode === 'all' && isRulePage + ? 'ring-1 ring-primary/40 shadow-sm' + : 'border border-slate-200 group-hover:border-slate-400 shadow-sm'; + + let fieldsLabel: React.ReactNode = null; + if (effThumbMode === 'filtered' && activePoint) { + const fs = getFieldsOnPage(activePoint, p - pageOffset); + const txt = fs.length ? fs.join(' · ') : '规则锚定页'; + fieldsLabel = ( +
+ {txt} +
+ ); + } + + return ( + + ); + }) + )} +
+
+ )} + + {/* ── 视口(单页) ── */} +
+
+ {numPages !== null && ( + <> + 页面加载中…
} + /> + {mainHighlight && ( + + + {`高亮文本: ${mainHighlight.text}`} + + + )} + + )} +
+
+
+ + + ); +} diff --git a/app/routes/reviews.tsx b/app/routes/reviews.tsx index a3b1541..273ad14 100644 --- a/app/routes/reviews.tsx +++ b/app/routes/reviews.tsx @@ -872,42 +872,13 @@ export default function ReviewDetails() { <> {/* 自定义面包屑 */}
- + /> */} {/* 在面包屑右侧显示精简版的FileInfo */}
- {/* 评分tag:优秀、合格、不合格、待评价 */} - {/* {(() => { - const score = ""; - let tagText = '待评价'; - let tagClassName = 'bg-gray-100 text-gray-600 border-gray-300'; - - if (score >= 90) { - tagText = '优秀'; - tagClassName = 'bg-green-50 text-green-700 border-green-300'; - } else if (score >= 60) { - tagText = '合格'; - tagClassName = 'bg-blue-50 text-blue-700 border-blue-300'; - } else if (score > 0) { - tagText = '不合格'; - tagClassName = 'bg-red-50 text-red-700 border-red-300'; - } - - return ( - - {tagText} - - ); - })()} */} - {reviewData.fileInfo.fileName} @@ -929,29 +900,8 @@ export default function ReviewDetails() {
- {/*
- 合同编号:{reviewData.fileInfo.contractNumber} - {reviewData.fileInfo.fileSize && ( - - | {reviewData.fileInfo.fileSize} | {reviewData.fileInfo.fileFormat} | {reviewData.fileInfo.pageCount}页  - - )} - {reviewData.fileInfo.uploadTime && ( -
- | 上传时间:{reviewData.fileInfo.uploadTime} | 上传用户:{reviewData.fileInfo.uploadUser} -
- )} -
*/} {/* 文件信息和操作按钮 */} - {/* */} - {/* 选项卡 */} { + if (bytes === 0) return "0 Bytes"; + const k = 1024; + const sizes = ["Bytes", "KB", "MB", "GB", "TB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; +}; + +// 定义统计数据类型 +interface Statistics { + total: number; + success: number; + warning: number; + error: number; + notApplicable: number; + score: number; +} + +// 定义文件信息类型 +interface FileInfo { + fileName: string; + contractNumber: string; + fileSize: string; + fileFormat: string; + pageCount: number; + uploadTime: string; + uploadUser: string; + auditStatus: number; + fileType: string; // 文件类型(1:合同,2:卷宗等) +} + +// 定义合同信息类型 +interface ContractInfo { + contractType: string; + signDate: string; + parties: { + partyA: string; + partyB: string; + }; + amount: string; + period: string; +} + +// 定义评查信息类型 +interface ReviewInfo { + reviewTime: string; + reviewModel: string; + ruleGroup: string; + result: string; + issueCount: number; +} + +// 定义文档内容类型 +interface FileContent { + title: string; + contractNumber: string; + parties: { + partyA: { + name: string; + address: string; + representative: string; + phone: string; + }; + partyB: { + name: string; + address: string; + representative: string; + phone: string; + }; + }; + sections: { + title: string; + content: string; + }[]; +} + +// 定义分析项类型 +interface AnalysisItem { + title: string; + content: string; + description: string; +} + +// 定义分析数据类型 +interface AnalysisData { + riskAlerts: AnalysisItem[]; + suggestions: AnalysisItem[]; + summary: string; +} + +// 定义评查数据类型 +interface ReviewData { + fileInfo: FileInfo; + contractInfo: ContractInfo; + reviewInfo: ReviewInfo; + statistics: Statistics; + fileContent: FileContent; + reviewPoints: ReviewPoint[]; + aiAnalysis: AnalysisData; +} + + +export const meta: MetaFunction = () => { + return [ + { title: "评查详情 - 中国烟草AI合同及卷宗审核系统" }, + { + name: "description", + content: "查看文档评查结果,处理问题点,确认评查结果" + } + ]; +}; + +export function links() { + return [{ rel: "stylesheet", href: reviewsStyles }]; +} + +export const handle = { + hideBreadcrumb: true +}; + +export async function loader({ request }: LoaderFunctionArgs) { + try { + const url = new URL(request.url); + const id = url.searchParams.get('id') || undefined; + 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不能为空' }); + } + + // 补充 pointCode 到 reviewPoints(直接查 DB,不受 Vite tree-shake 影响) + async function patchPointCodes(points: any[], jwt: string) { + try { + const pointIds = points.map((p: any) => p.pointId).filter(Boolean); + if (pointIds.length === 0) return; + const resp = await postgrestGet('/api/postgrest/proxy/evaluation_points', { + select: 'id,code', + filter: { id: `in.(${[...new Set(pointIds)].join(',')})` }, + token: jwt, + }); + // resp.data 可能是 {code:200, data:[...]} 或直接 [...] + const raw = resp.data; + const epList = Array.isArray(raw) ? raw : (raw?.data && Array.isArray(raw.data) ? raw.data : []); + const codeMap: Record = {}; + epList.forEach((ep: any) => { if (ep.code) codeMap[String(ep.id)] = ep.code; }); + points.forEach((p: any) => { p.pointCode = codeMap[String(p.pointId)] || ''; }); + } catch (e) { + console.error('[Reviews Loader] patchPointCodes error:', e); + } + } + + // 获取用户会话信息 + const { getUserSession } = await import("~/api/login/auth.server"); + const { userInfo, frontendJWT } = await getUserSession(request); + + // 🆕 使用新的统一API获取评查点数据 + // 先尝试新的统一评查接口 + const unifiedData = await getUnifiedEvaluationResults(id, request); + + // 如果统一接口返回错误或flow_type为legacy,使用原有API + if ('error' in unifiedData || !unifiedData.flow_type) { + console.log("[Reviews Loader] 统一接口不可用,使用旧接口..."); + 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) { + await patchPointCodes(reviewData.data as any[], frontendJWT); + 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 + }); + } + } + + // 统一接口成功返回,判断流程类型 + 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]; + await patchPointCodes(allReviewPoints, frontendJWT); + + 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 流程但统一接口可用,也走原有逻辑 + 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 + }); + } + } 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 : '未知错误'}` }); + } +} + +// 添加 action 函数处理需要用户认证的操作 +export async function action({ request }: ActionFunctionArgs) { + try { + const formData = await request.formData(); + const intent = formData.get("intent") as string; + + // console.log('Action接收到请求, intent:', intent); + + if (intent === "updateReviewResult") { + const reviewPointResultId = formData.get("reviewPointResultId") as string; + const editAuditStatusId = formData.get("editAuditStatusId") as string; + const result = formData.get("result") as string; + const message = formData.get("message") as string; + + // console.log('更新评查结果参数:', { reviewPointResultId, editAuditStatusId, result, message }); + + try { + const response = await updateReviewResult(reviewPointResultId, editAuditStatusId, result, message, request); + + if (response.error) { + console.error('updateReviewResult返回错误:', response.error); + return Response.json({ success: false, error: response.error }, { status: response.status || 500 }); + } + + return Response.json({ success: true, data: response.data, intent: "updateReviewResult" }); + } catch (updateError) { + console.error('调用updateReviewResult时发生异常:', updateError); + return Response.json({ + success: false, + error: updateError instanceof Error ? updateError.message : '更新评查结果时发生未知错误' + }, { status: 500 }); + } + } + + if (intent === "confirmReviewResults") { + const documentId = formData.get("documentId") as string; + + // console.log('确认评查结果参数:', { documentId }); + + try { + const response = await confirmReviewResults(documentId, request); + + if (response.error) { + console.error('confirmReviewResults返回错误:', response.error); + return Response.json({ success: false, error: response.error, intent: "confirmReviewResults" }, { status: response.status || 500 }); + } + + return Response.json({ success: true, data: response.data, intent: "confirmReviewResults" }); + } catch (confirmError) { + console.error('调用confirmReviewResults时发生异常:', confirmError); + return Response.json({ + success: false, + error: confirmError instanceof Error ? confirmError.message : '确认评查结果时发生未知错误', + intent: "confirmReviewResults" + }, { status: 500 }); + } + } + + console.error('收到未知的操作类型:', intent); + return Response.json({ success: false, error: "未知的操作类型" }, { status: 400 }); + } catch (error) { + console.error('Action处理失败:', error); + return Response.json({ + success: false, + error: error instanceof Error ? error.message : '操作失败' + }, { status: 500 }); + } +} + +export default function ReviewDetails() { + const navigate = useNavigate(); + const loaderData = useLoaderData(); + + const fetcher = useFetcher(); + const { document, reviewPoints, statistics, reviewInfo, comparison_document, frontendJWT } = loaderData; + + const [isLoading, setIsLoading] = useState(false); // 已经通过loader加载了数据,不需要再显示加载状态 + const [activeTab, setActiveTab] = useState('preview'); // 'preview', 'analysis', 'fileinfo' + const [reviewData, setReviewData] = useState(null); + const [activeReviewPointResultId, setActiveReviewPointResultId] = useState(null); + const [targetPage, setTargetPage] = useState(undefined); + const [templateTargetPage, setTemplateTargetPage] = useState(undefined); + const [charPositions, setCharPositions] = useState | undefined>(undefined); + const [highlightValue, setHighlightValue] = useState(undefined); + const [pendingUpdate, setPendingUpdate] = useState<{ + reviewPointResultId: string; + newStatus: string; + message: string; + } | null>(null); + const [aiSuggestionReplace, setAiSuggestionReplace] = useState<{ + searchText: string; + replaceText: string; + pageNumber: number; + } | undefined>(undefined); + + // FilePreview 组件的 ref,用于在下载前保存文档 + const filePreviewRef = useRef(null); + + // CollaboraViewer 组件的 key,用于强制重新加载触发保存 + const [collaboraKey, setCollaboraKey] = useState(0); + + // 保存文档的回调函数,传递给 ReviewTabs + // 通过改变 key 强制重新加载 CollaboraViewer 组件,触发组件卸载时的保存逻辑 + const handleSaveBeforeDownload = useCallback(async (): Promise => { + // 检查文件类型是否为 DOCX(需要 Collabora 保存) + const fileExtension = document?.path?.split('.').pop()?.toLowerCase(); + if (fileExtension !== 'docx') { + // 非 DOCX 文件不需要保存 + return true; + } + + const collaboraRef = filePreviewRef.current?.collaboraViewerRef?.current; + if (!collaboraRef?.isReady) { + console.log('[Reviews] Collabora 未就绪,跳过保存'); + return true; + } + + try { + // console.log('[Reviews] 通过重新加载 CollaboraViewer 保存文档...'); + + // 改变 key 触发组件卸载(会执行保存)和重新挂载 + setCollaboraKey(prev => prev + 1); + + // 等待组件重新加载完成 + // 先等待组件卸载和重新挂载 + await new Promise(resolve => setTimeout(resolve, 500)); + + // 轮询检查组件是否重新加载完成 + const maxWaitTime = 30000; // 最大等待30秒 + const checkInterval = 500; // 每500ms检查一次 + let waitedTime = 0; + + while (waitedTime < maxWaitTime) { + const newCollaboraRef = filePreviewRef.current?.collaboraViewerRef?.current; + if (newCollaboraRef?.isReady) { + // console.log('[Reviews] CollaboraViewer 重新加载完成'); + // 额外等待一小段时间确保文档完全就绪 + await new Promise(resolve => setTimeout(resolve, 500)); + return true; + } + await new Promise(resolve => setTimeout(resolve, checkInterval)); + waitedTime += checkInterval; + } + + console.warn('[Reviews] 等待 CollaboraViewer 重新加载超时'); + return true; // 超时也允许下载 + } catch (error) { + console.error('[Reviews] 保存文档失败:', error); + toastService.error('保存文档失败,请重试'); + return false; + } + }, [document?.path]); + + // 🐛 调试:打印 loader 返回的完整数据到浏览器控制台 + // useEffect(() => { + // if (typeof window !== 'undefined') { + // console.group('📦 [Reviews] Loader 数据'); + // console.log('完整数据:', loaderData); + // console.log('文档信息:', document); + // console.log('评查点数量:', reviewPoints?.length); + // console.log('评查点数量:', reviewPoints); + // console.log('统计信息:', statistics); + // 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]); + + // 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]); + + + // 当文档 ID 变化时,清空高亮相关的状态 + useEffect(() => { + if (document?.id) { + // console.log('[Reviews] 文档ID变化,清空高亮状态'); + setActiveReviewPointResultId(null); + setTargetPage(undefined); + setTemplateTargetPage(undefined); + setCharPositions(undefined); + setHighlightValue(undefined); + } + }, [document?.id]); + + // 模拟获取评查数据 + useEffect(() => { + 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 handleTabChange = (tabKey: string) => { + setActiveTab(tabKey); + }; + + const handleReviewPointSelect = (reviewPointId: string, page?: number, charPos?: Array<{ box: number[][], char: string, score: number }>, value?: string) => { + // 如果点击的是相同的评查点,但有page参数,先重置targetPage以确保useEffect能够触发 + if (reviewPointId === activeReviewPointResultId && page) { + setTargetPage(undefined); + setCharPositions(undefined); + setHighlightValue(undefined); + // 使用setTimeout确保状态更新后再设置新的targetPage、charPositions和highlightValue + setTimeout(() => { + setActiveReviewPointResultId(reviewPointId); + setTargetPage(page); + setCharPositions(charPos); + setHighlightValue(value); + }, 0); + } else { + // 正常设置activeReviewPointId、targetPage、charPositions和highlightValue + setActiveReviewPointResultId(reviewPointId); + setTargetPage(page); + setCharPositions(charPos); + setHighlightValue(value); + } + }; + + // 处理AI建议替换 + const handleAiSuggestionReplace = (searchText: string, replaceText: string, pageNumber: number) => { + // console.log('[Reviews] AI建议替换:', { searchText, replaceText, pageNumber }); + // 设置替换参数,触发 CollaboraViewer 的搜索替换 + setAiSuggestionReplace({ + searchText, + replaceText, + pageNumber + }); + // 短暂延迟后清除参数,以便下次可以重新触发 + setTimeout(() => { + setAiSuggestionReplace(undefined); + }, 500); + }; + + // 刷新评审数据 + // async function refreshReviewData(documentId: string) { + // // 设置加载状态 + // setIsLoading(true); + // try { + // // 获取最新的评审数据 + // const response = await getReviewPoints(documentId); + + // if ('error' in response && response.error) { + // console.error('刷新评审数据失败:', response.error); + // toastService.error(`刷新评审数据失败: ${response.error}`); + // return; + // } + + // // 确保response有效且具有预期的属性 + // if ('data' in response && 'stats' in response && 'reviewInfo' in response) { + // const reviewPointsData = response.data || []; + // const statisticsData = response.stats || { total: 0, success: 0, warning: 0, error: 0, score: 0 }; + // const reviewInfoData = response.reviewInfo || { + // reviewTime: '', + // reviewModel: '', + // ruleGroup: '', + // result: '', + // issueCount: 0 + // }; + + // // 更新评审数据和统计信息 + // setReviewData(prevData => { + // if (!prevData) { + // // 如果prevData为null,创建一个新的ReviewData对象 + // return { + // fileInfo: { + // fileName: document?.name || "", + // contractNumber: document?.documentNumber || "", + // fileSize: document?.size ? formatFileSize(document.size) : "", + // fileFormat: document?.fileType ? document.fileType.toUpperCase() : "", + // pageCount: document?.pageCount || 0, + // uploadTime: document?.uploadTime || "", + // uploadUser: document?.uploadUser || "", + // auditStatus: document?.auditStatus || 0 + // }, + // contractInfo: getMockReviewData().contractInfo, + // reviewInfo: reviewInfoData as ReviewInfo, + // statistics: statisticsData as Statistics, + // fileContent: getMockReviewData().fileContent, + // reviewPoints: reviewPointsData as unknown as ReviewPoint[], + // aiAnalysis: getMockReviewData().aiAnalysis + // }; + // } + + // // 处理prevData非null的情况 + // return { + // ...prevData, + // reviewPoints: reviewPointsData as unknown as ReviewPoint[], + // statistics: statisticsData as Statistics, + // reviewInfo: reviewInfoData as ReviewInfo + // }; + // }); + + // toastService.success('评审数据已更新'); + // } else { + // console.error('返回的数据格式不正确'); + // toastService.error('刷新评审数据失败: 返回的数据格式不正确'); + // } + // } catch (error) { + // console.error('刷新评审数据失败:', error); + // toastService.error(`刷新评审数据失败: ${error instanceof Error ? error.message : '未知错误'}`); + // } finally { + // setIsLoading(false); + // } + // } + + // 监听fetcher状态变化 + useEffect(() => { + if (fetcher.state === "idle" && fetcher.data && pendingUpdate) { + const result = fetcher.data as { success: boolean; error?: string; data?: unknown }; + // console.log('Fetcher返回数据:', result); + + if (result.success) { + // console.log('评查点状态更新成功'); + + // 使用pendingUpdate中的参数更新本地状态 + if (reviewData && pendingUpdate.reviewPointResultId) { + const reviewPointToUpdate = reviewData.reviewPoints.find(point => point.id === pendingUpdate.reviewPointResultId); + const oldStatus = reviewPointToUpdate?.status || ''; + const wasSuccess = reviewPointToUpdate?.result === true; + const newIsSuccess = pendingUpdate.newStatus === 'true'; + + // 更新评查点 + const updatedReviewPoints = reviewData.reviewPoints.map(point => + point.id === pendingUpdate.reviewPointResultId ? { + ...point, + result: pendingUpdate.newStatus === 'true' ? true : (pendingUpdate.newStatus === 'false' ? false : point.result), + editAuditStatus: pendingUpdate.newStatus === 'review' ? 0 : 1, + title: pendingUpdate.newStatus === 'review' ? point.title : pendingUpdate.message, + editAuditStatusMessage: pendingUpdate.newStatus === 'review' ? point.editAuditStatusMessage : pendingUpdate.message + } : point + ); + + // 更新统计数据 + const updatedStatistics = { ...reviewData.statistics }; + + // 只处理结果实际变化的情况 + if (pendingUpdate.newStatus !== 'review' && wasSuccess !== newIsSuccess) { + if (newIsSuccess) { + // 从不通过变为通过 + updatedStatistics.success += 1; + if (oldStatus === 'warning') { + updatedStatistics.warning = Math.max(0, updatedStatistics.warning - 1); + } else if (oldStatus === 'error') { + updatedStatistics.error = Math.max(0, updatedStatistics.error - 1); + } + } else { + // 从通过变为不通过 + updatedStatistics.success = Math.max(0, updatedStatistics.success - 1); + if (oldStatus === 'warning') { + updatedStatistics.warning += 1; + } else if (oldStatus === 'error') { + updatedStatistics.error += 1; + } + } + } + + // 更新 UI 状态 + setReviewData({ + ...reviewData, + reviewPoints: updatedReviewPoints, + statistics: updatedStatistics + }); + } + + if(document && document.id && pendingUpdate.newStatus !== 'review'){ + toastService.success('评查点状态已更新'); + } + + // 清除pendingUpdate + setPendingUpdate(null); + } else { + console.error('更新评查结果失败:', result.error); + toastService.error(`更新评查结果失败: ${result.error || '未知错误'}`); + // 清除pendingUpdate + setPendingUpdate(null); + } + } + }, [fetcher.state, fetcher.data, pendingUpdate, document, reviewData]); + + // 监听fetcher状态变化 - 处理确认评查结果 + useEffect(() => { + if (fetcher.state === "idle" && fetcher.data && !pendingUpdate) { + const result = fetcher.data as { success: boolean; error?: string; intent?: string }; + + // 只处理confirmReviewResults的响应 + if (result.intent === 'confirmReviewResults') { + setIsLoading(false); + + if (result.success) { + toastService.success('评查结果已确认,文档审核状态已更新'); + // 导航到文档列表页 + navigate('/documents/list'); + } else { + console.error('确认评查结果失败:', result.error); + toastService.error(`确认评查结果失败: ${result.error || '未知错误'}`); + } + } + } + }, [fetcher.state, fetcher.data, pendingUpdate, navigate]); + + // 处理评审点状态变更 + const handleReviewPointStatusChange = async (reviewPointResultId: string, editAuditStatusId: string | number, newStatus: string, message: string) => { + // 将字符串的布尔值转换为布尔类型 + let boolResult = 'review'; + if(newStatus !== 'review'){ + boolResult = newStatus === 'true' ? 'true' : 'false'; + } + + try { + // console.log('开始提交评查结果更新:', { reviewPointResultId, editAuditStatusId, boolResult, message }); + + // 设置待处理的更新信息 + setPendingUpdate({ + reviewPointResultId, + newStatus: boolResult, + message + }); + + // 使用 Remix 的 useFetcher 调用 action + const formData = new FormData(); + formData.append("intent", "updateReviewResult"); + formData.append("reviewPointResultId", reviewPointResultId); + formData.append("editAuditStatusId", editAuditStatusId.toString()); + formData.append("result", boolResult); + formData.append("message", message); + + fetcher.submit(formData, { method: "POST" }); + + // console.log('请求已提交,等待响应...'); + + // 注意:本地状态更新现在在useEffect中处理,当fetcher返回成功响应时触发 + } catch (error) { + console.error('更新评查结果出错:', error); + toastService.error('更新评查结果失败,请稍后重试'); + } + }; + + const handleConfirmResults = async () => { + if (!document || !document.id) { + toastService.error('文档数据不完整,无法确认评查结果'); + return; + } + + try { + // 显示加载状态 + setIsLoading(true); + + // 使用 Remix 的 useFetcher 调用 action + const formData = new FormData(); + formData.append("intent", "confirmReviewResults"); + formData.append("documentId", document.id.toString()); + + fetcher.submit(formData, { method: "POST" }); + + } catch (error) { + console.error('确认评查结果出错:', error); + toastService.error(`确认评查结果失败: ${error instanceof Error ? error.message : '未知错误'}`); + setIsLoading(false); + } + }; + + // 构建自定义面包屑项 + const getBreadcrumbItems = () => { + const items = [ + { title: "评查详情", to: `/reviews?id=${document?.id}` } + ]; + + // 添加前置路由 + if (loaderData.previousRoute) { + if (loaderData.previousRoute === 'filesUpload') { + items.unshift({ title: "文件上传", to: "/files/upload" }); + } else if (loaderData.previousRoute === 'documents') { + items.unshift({ title: "文档列表", to: "/documents/list" }); + } else if (loaderData.previousRoute === 'rulesFiles') { + items.unshift({ title: "评查文件列表", to: "/rules-files" }); + } + } + + return items; + }; + + return ( +
+ {isLoading ? ( +
+
+ 加载中... +
+ ) : reviewData && ( + <> + {/* 自定义面包屑 */} +
+ {/* */} + + {/* 在面包屑右侧显示精简版的FileInfo */} +
+ + {reviewData.fileInfo.fileName} + +
+ {/* 合同编号:{reviewData.fileInfo.contractNumber} */} + { reviewData.fileInfo.fileType != "1" ? "卷宗" : "合同" } + 编号:{reviewData.fileInfo.contractNumber} + {reviewData.fileInfo.fileSize && ( + + | {reviewData.fileInfo.fileSize} | {reviewData.fileInfo.fileFormat} | {reviewData.fileInfo.pageCount}页  + + )} + {reviewData.fileInfo.uploadTime && ( +
+ | 上传时间:{reviewData.fileInfo.uploadTime} + {/* | 上传用户:{reviewData.fileInfo.uploadUser} */} +
+ )} +
+
+
+ + {/* 文件信息和操作按钮 */} + {/* 选项卡 */} + + {/* 评查结果选项卡内容 */} + {activeTab === 'preview' && ( +
+ {/* {JSON.stringify(document)} */} + {/* 左侧:文件预览 */} +
+ {(() => { + // console.log('[Reviews] 准备渲染FilePreview', { + // hasDocument: !!document, + // documentPath: document?.path, + // targetPage, + // hasCharPositions: !!charPositions, + // charPositionsLength: charPositions?.length + // }); + return ( + + ); + })()} +
+ + {/* 右侧:评查结果 */} +
+ {/* {JSON.stringify(reviewData.fileInfo.fileFormat)} */} + +
+
+ )} + + {/* 结构比对选项卡内容 */} + {activeTab === 'filecompare' && ( +
+ {/* {JSON.stringify(comparison_document?.template_contract_path)} -----{JSON.stringify(document?.path)} */} + +
+ )} + + {/* 原来的结构比对选项卡内容(已注释) */} + {/* {activeTab === 'filecompare' && ( +
+
+ +
+ +
+ +
+ +
+ { + if (sourcePage > 0) { + if (sourcePage === targetPage) { + setTargetPage(undefined); + setTimeout(() => setTargetPage(sourcePage), 0); + } else { + setTargetPage(sourcePage); + } + console.log(`跳转到主文件第${sourcePage}页`); + } + if (templatePage > 0) { + if (templatePage === templateTargetPage) { + setTemplateTargetPage(undefined); + setTimeout(() => setTemplateTargetPage(templatePage), 0); + } else { + setTemplateTargetPage(templatePage); + } + console.log(`跳转到模板文件第${templatePage}页`); + } + }} + /> +
+
+ )} */} + + {/* AI智能分析选项卡内容 */} + {activeTab === 'analysis' && ( + + )} + + {/* 文件信息选项卡内容 */} + {activeTab === 'fileinfo' && ( + + )} +
+ + )} +
+ ); +} + +// 模拟评查数据 +function getMockReviewData(): ReviewData { + return { + fileInfo: { + fileName: "烟草产品销售合同(2023版).docx", + contractNumber: "XS-2023-1025-001", + fileSize: "5.2MB", + fileFormat: "DOCX", + pageCount: 5, + uploadTime: "2023-10-25 14:30:45", + uploadUser: "张三", + auditStatus: 0, + fileType: "1" + }, + contractInfo: { + contractType: "销售合同", + signDate: "2023年10月20日", + parties: { + partyA: "XX烟草公司", + partyB: "YY贸易有限公司" + }, + amount: "¥ 1,580,000.00", + period: "2023年11月1日至2024年10月31日" + }, + reviewInfo: { + reviewTime: "2023-10-25 14:35:12", + reviewModel: "DeepSeek", + ruleGroup: "合同标准规则组", + result: "warning", + issueCount: 9 + }, + statistics: { + total: 15, + success: 6, + warning: 7, + error: 2, + score: 75 + }, + fileContent: { + title: "烟草产品销售合同", + contractNumber: "XS-2023-1025-001", + parties: { + partyA: { + name: "XX烟草公司", + address: "XX省XX市XX区XX路XX号", + representative: "张XX", + phone: "123-4567-8901" + }, + partyB: { + name: "YY贸易有限公司", + address: "XX省XX市XX区YY路YY号", + representative: "李YY", + phone: "123-4567-8902" + } + }, + sections: [ + { + title: "总则", + content: "1.1 本合同适用于甲乙双方之间的烟草制品买卖事宜。\n1.2 双方应本着平等互利、诚实信用的原则履行本合同。" + }, + { + title: "合同标的物", + content: "2.1 产品名称:烟草制品\n2.2 规格型号:如附件所列\n2.3 数量:5000箱\n2.4 质量要求:符合国家标准GB/T XXXXX-XXXX" + }, + { + title: "交货与付款", + content: "3.1 交货时间:自合同签订之日起30日内。\n3.2 乙方应在收到货物之日起5个工作日内支付合同款项,甲方应在收到乙方全部付款后开具增值税专用发票,乙方应在收到发票后支付剩余款项。\n3.3 交货地点:乙方指定的仓库。\n3.4 运输方式:陆运,运费由甲方承担。" + }, + { + title: "合同文本", + content: "本合同一式两份,甲乙双方各执一份,具有同等法律效力。" + } + ] + }, + reviewPoints: [ + { + id: "1", + pointName: "付款条款", + title: "付款条件描述不明确", + groupName: "付款条款清晰性", + // location: "交货与付款条款", + status: "error", + editAuditStatus: 0, + content: { + 'anjia':{ + page: 1, + value: { text: "乙方应在收到货物之日起5个工作日内支付合同款项,甲方应在收到乙方全部付款后开具增值税专用发票,乙方应在收到发票后支付剩余款项。" } + }, + 'yijia':{ + page: 1, + value: { text: "乙方应在收到货物之日起5个工作日内支付合同款项,甲方应在收到乙方全部付款后开具增值税专用发票,乙方应在收到发票后支付剩余款项。" } + } + }, + suggestion: "乙方应在收到货物验收合格之日起5个工作日内支付合同总额的70%,甲方收到该部分款项后3个工作日内向乙方开具等额增值税专用发票;乙方应在收到发票之日起5个工作日内支付剩余30%款项。", + position: { section: "交货与付款", index: 2 }, + result: false + }, + { + id: "2", + pointName: "违约责任", + title: "违约责任条款缺失", + groupName: "合同权利义务对等性", + status: "warning", + editAuditStatus: 0, + content: { + 'clause': { + page: 1, + value: { text: "如合同发生纠纷,双方应协商解决。" } + } + }, + suggestion: "如合同发生纠纷,双方应友好协商解决;协商不成的,任何一方均有权向甲方所在地人民法院提起诉讼。任何一方未能履行本合同约定义务,应向守约方支付合同总金额的10%作为违约金;给对方造成损失的,还应赔偿由此产生的全部损失。", + position: { section: "争议解决", index: 0 }, + result: false + }, + { + id: "3", + pointName: "签章审核", + title: "签章不完整", + groupName: "合同签署规范性", + status: "warning", + editAuditStatus: 0, + content: { + 'signature': { + page: 5, + value: { text: "乙方(盖章):YY贸易有限公司\n代表人签字:李YY\n日期:2023年10月20日" } + } + }, + suggestion: "需要联系甲方补充公章", + needsHumanReview: true, + humanReviewNote: "需要联系甲方补充公章", + position: { section: "签章", index: 0 }, + result: false + }, + { + id: "9", + pointName: "交货方式", + title: "交货方式描述模糊", + groupName: "履行条款明确性", + status: "success", + editAuditStatus: 0, + content: { + 'delivery': { + page: 3, + value: { text: "3.4 运输方式:陆运,运费由甲方承担。" } + } + }, + suggestion: "建议补充具体的运输方式和时间", + needsHumanReview: true, + humanReviewNote: "经核实,该交货方式虽然描述不够详细,但符合行业惯例且双方已经多次合作,不会造成实际履行障碍。", + humanReviewBy: "王法务", + humanReviewTime: "2023-11-05 14:30:22", + position: { section: "交货与付款", index: 4 }, + result: true + }, + { + id: "10", + pointName: "法律适用", + title: "法律适用条款缺失", + groupName: "争议解决条款完整性", + status: "error", + editAuditStatus: 0, + content: { + 'missing': { + page: 0, + value: { text: "" } + } + }, + suggestion: "第十三条 法律适用\n本合同的订立、效力、解释、履行及争议的解决均适用中华人民共和国法律。因本合同引起的或与本合同有关的任何争议,双方应友好协商解决。协商不成的,提交甲方所在地人民法院诉讼解决。", + position: { section: "缺失", index: 0 }, + result: false + } + ], + aiAnalysis: { + riskAlerts: [ + { + title: "风险提示", + content: "本合同缺少违约责任条款,可能导致权责不明。", + description: "根据《中华人民共和国民法典》第五百七十七条规定,建议增加违约责任条款,明确双方违约责任及赔偿方式。" + }, + { + title: "完整性检查", + content: "本合同缺少法律适用条款。", + description: "根据行业惯例,销售合同应明确约定适用法律和纠纷解决方式,以避免后续争议解决时的不确定性。" + } + ], + suggestions: [ + { + title: "优化建议", + content: "建议完善付款条件描述。", + description: "目前合同中关于付款条件的描述存在歧义,可能导致付款时间和条件不明确。建议按系统修改建议优化。" + } + ], + summary: "本合同基本结构完整,主体内容清晰,但存在多处条款描述不完善的问题,主要体现在支付条件、违约责任、不可抗力、保密条款、合同终止条件等方面。这些问题虽不影响合同的基本合规性,但可能在合同履行过程中引发争议和纠纷。同时,合同签章不完整,也影响了合同的法律效力。建议对上述问题进行修改完善后再行签署。" + } + }; +} diff --git a/docs/design/7c-简化C-极简(1).html b/docs/design/7c-简化C-极简(1).html new file mode 100644 index 0000000..23b84f1 --- /dev/null +++ b/docs/design/7c-简化C-极简(1).html @@ -0,0 +1,1435 @@ + + + + + +C · 极致简化 · LeAudit + + + + + + + + + + + +
+ + + +
+ + + + + +
+ +
+
+ + | + + 22 / 62 + + | + + 100% + +
+
+ 当前高亮: + + MM-017 · 验收条款 + + + +
+
+ + +
+ +
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+
+ +
+ + + + +
+ +
+ + + +