diff --git a/app/api/cross-checking/cross-files.ts b/app/api/cross-checking/cross-files.ts index e483dcc..3bae282 100644 --- a/app/api/cross-checking/cross-files.ts +++ b/app/api/cross-checking/cross-files.ts @@ -1,11 +1,11 @@ import { API_BASE_URL } from '../../config/api-config'; -import { postgrestPut } from '../postgrest-client'; +import { postgrestPut, postgrestGet } from '../postgrest-client'; import axios from 'axios'; // 交叉评查任务状态枚举 export enum CrossCheckingTaskStatus { PENDING = 'pending', - IN_PROGRESS = 'in_progress', + IN_PROGRESS = 'in_progress', COMPLETED = 'completed' } @@ -21,6 +21,13 @@ export enum CrossCheckingDocType { PERMIT = 'permit' // 行政许可 } +// 文档类型接口(用于交叉评查案卷类型选项) +export interface DocumentType { + id: number; + name: string; + evaluation_point_groups_ids?: number[]; +} + // 交叉评查任务接口 export interface CrossCheckingTask { id: number; @@ -491,11 +498,11 @@ export async function updateDocumentAuditStatus(id: string, auditStatus: number, }, frontendJWT ); - + if (response.error) { return { error: response.error, status: response.status }; } - + return { success: true }; } catch (error) { console.error('更新文件审核状态失败:', error); @@ -504,4 +511,53 @@ export async function updateDocumentAuditStatus(id: string, auditStatus: number, status: 500 }; } +} + +/** + * 获取可用于交叉评查的文档类型列表 + * 条件:evaluation_point_groups_ids 不为空 + * @param jwtToken JWT token + * @returns 文档类型列表 + */ +export async function getCrossCheckingDocumentTypes(jwtToken?: string): Promise> { + try { + console.log('[getCrossCheckingDocumentTypes] 开始获取交叉评查文档类型'); + + const response = await postgrestGet('document_types',{ + select: 'id,name,evaluation_point_groups_ids', + filter: { + evaluation_point_groups_ids: 'not.is.null' + }, + token: jwtToken + }); + + if (response.error) { + console.error('[getCrossCheckingDocumentTypes] 获取失败:', response.error); + return { + success: false, + error: response.error + }; + } + + // 进一步过滤,确保 evaluation_point_groups_ids 是非空数组 + const dataArray = Array.isArray(response.data) ? response.data : []; + const filteredData = dataArray.filter( + (item: DocumentType) => item.evaluation_point_groups_ids && + Array.isArray(item.evaluation_point_groups_ids) && + item.evaluation_point_groups_ids.length > 0 + ); + + console.log('[getCrossCheckingDocumentTypes] 获取成功,共', filteredData.length, '个文档类型'); + + return { + success: true, + data: filteredData + }; + } catch (error) { + console.error('[getCrossCheckingDocumentTypes] 获取交叉评查文档类型失败:', error); + return { + success: false, + error: error instanceof Error ? error.message : '获取文档类型失败' + }; + } } \ No newline at end of file diff --git a/app/components/collabora/CallCustomScript.ts b/app/components/collabora/CallCustomScript.ts index 6d101de..4a4fc0b 100644 --- a/app/components/collabora/CallCustomScript.ts +++ b/app/components/collabora/CallCustomScript.ts @@ -42,7 +42,7 @@ export async function callPythonScript( const verbose = options?.verbose ?? true; if (verbose) { - console.log('[CallCustomScript] 调用 Python 脚本:', { scriptFile, functionName, args }); + // console.log('[CallCustomScript] 调用 Python 脚本:', { scriptFile, functionName, args }); } return new Promise((resolve, reject) => { @@ -72,7 +72,7 @@ export async function callPythonScript( cleanup(); if (verbose) { - console.log('[CallCustomScript] 收到 Python 脚本响应:', data); + // console.log('[CallCustomScript] 收到 Python 脚本响应:', data); } const result = parseScriptResponse(data, verbose); @@ -107,7 +107,7 @@ export async function callPythonScript( }; if (verbose) { - console.log('[CallCustomScript] 发送 PostMessage:', message); + // console.log('[CallCustomScript] 发送 PostMessage:', message); } iframeWindow.postMessage(JSON.stringify(message), '*'); @@ -130,12 +130,12 @@ function parseScriptResponse(data: PostMessageResponse, verbose: boolean): Scrip } if (verbose) { - console.log('[CallCustomScript] 解析结果:', { - commandName: values.commandName, - unoSuccess: values.success, - resultRaw: values.result, - resultExtracted: resultValue, - }); + // console.log('[CallCustomScript] 解析结果:', { + // commandName: values.commandName, + // unoSuccess: values.success, + // resultRaw: values.result, + // resultExtracted: resultValue, + // }); } if (values.success === false) { diff --git a/app/components/collabora/CollaboraViewer.tsx b/app/components/collabora/CollaboraViewer.tsx index aaa6f42..7af2a86 100644 --- a/app/components/collabora/CollaboraViewer.tsx +++ b/app/components/collabora/CollaboraViewer.tsx @@ -10,11 +10,11 @@ * @encoding UTF-8 */ -import { useRef, forwardRef, useImperativeHandle, useState } from 'react'; +import { useRef, forwardRef, useImperativeHandle, useState, useEffect } from 'react'; import type { CollaboraViewerProps, CollaboraViewerHandle } from './types'; import { useCollaboraConfig, useDocumentReady, useCollaboraUnoCommands } from './hooks'; import { sendUnoCommand } from './Uno'; -import { highlightText } from './lib/Highlightselecttext'; +import { highlightText as performTextHighlight } from './lib/Highlightselecttext'; import { clearHighlights } from './lib/ClearHighlight'; import { unoScrollToTop, @@ -41,10 +41,14 @@ export const CollaboraViewer = forwardRef(null); + // 保存 iframe 的 contentWindow 引用,用于组件卸载时清除高亮 + const iframeWindowRef = useRef(null); // 高亮测试面板状态 const [highlightTextInput, setHighlightTextInput] = useState(''); @@ -89,17 +93,119 @@ export const CollaboraViewer = forwardRef { + if (isDocumentLoaded && iframeRef.current?.contentWindow) { + iframeWindowRef.current = iframeRef.current.contentWindow; + console.log('[CollaboraViewer] 已保存 iframe window 引用'); + + // 🔥 文档加载完成后主动清除一次高亮(防止缓存的高亮状态) + console.log('[CollaboraViewer] 🧹 文档加载完成,清除可能存在的缓存高亮'); + clearHighlights(iframeRef.current.contentWindow, { + color: 16776960, + timeout: 5000, + }).then((result) => { + if (result.count && result.count > 0) { + console.log(`[CollaboraViewer] ✓ 清除了 ${result.count} 个缓存的高亮区域`); + } else { + console.log('[CollaboraViewer] ✓ 文档无缓存高亮,已确认干净'); + } + }).catch(error => { + console.warn('[CollaboraViewer] ⚠️ 清除缓存高亮失败:', error); + }); + } + }, [isDocumentLoaded]); + // 3. UNO 命令封装 const unoCommands = useCollaboraUnoCommands(iframeRef); - // 4. 暴露接口给父组件 + // 4. 暴露接口给父组件(包括清除高亮方法) useImperativeHandle(ref, () => ({ unoCommands, isReady: isDocumentLoaded, mode, getIframeWindow: () => iframeRef.current?.contentWindow || null, + clearAllHighlights: async () => { + const savedWindow = iframeWindowRef.current || iframeRef.current?.contentWindow; + if (savedWindow) { + console.log('[CollaboraViewer] 🧹 父组件调用清除高亮'); + await clearHighlights(savedWindow, { + color: 16776960, + timeout: 5000, + }); + console.log('[CollaboraViewer] ✓ 清除高亮完成'); + } else { + console.warn('[CollaboraViewer] ⚠️ 无法清除高亮:iframe window 不可用'); + } + }, }), [unoCommands, isDocumentLoaded, mode]); + // 5. 监听 targetPage 和 highlightText 变化,自动跳转并高亮 + useEffect(() => { + // 如果文档未加载完成,不执行跳转和高亮 + if (!isDocumentLoaded || !iframeRef.current?.contentWindow) { + return; + } + + // 如果有高亮文本,执行高亮操作 + if (highlightText && highlightText.trim() !== '') { + const performHighlight = async () => { + try { + const iframeWindow = iframeRef.current!.contentWindow!; + const textToHighlight = highlightText.trim(); + + // 🔥 在高亮新内容之前,先清除之前的所有高亮 + console.log('[CollaboraViewer] 清除旧高亮...'); + await clearHighlights(iframeWindow, { + color: 16776960, // 黄色 + timeout: 5000, + }); + + // 短暂延迟后执行新高亮,确保清除操作完成 + await new Promise(resolve => setTimeout(resolve, 100)); + + // 执行新高亮 + await performTextHighlight( + iframeWindow, + textToHighlight, + { page: targetPage } + ); + console.log(`[CollaboraViewer] 已高亮文本: "${textToHighlight}"${targetPage ? ` (第${targetPage}页)` : ''}`); + } catch (error) { + console.error('[CollaboraViewer] 高亮失败:', error); + } + }; + + performHighlight(); + } + }, [targetPage, highlightText, isDocumentLoaded]); + + // 6. 组件销毁时清除所有高亮(使用保存的 window 引用) + useEffect(() => { + // 返回清理函数,在组件卸载时执行 + return () => { + const savedWindow = iframeWindowRef.current; + if (savedWindow) { + console.log('[CollaboraViewer] 🔥 组件即将销毁,立即清除所有高亮'); + + // 立即触发清除操作,不等待异步完成 + // 使用 void 关键字表示我们不关心 Promise 的结果 + void clearHighlights(savedWindow, { + color: 16776960, // 黄色 + timeout: 3000, + }).then(() => { + console.log('[CollaboraViewer] ✓ 组件销毁时高亮清除成功'); + }).catch(error => { + console.error('[CollaboraViewer] ✗ 组件销毁时清除高亮失败:', error); + }); + + // 清空引用 + iframeWindowRef.current = null; + } else { + console.warn('[CollaboraViewer] ⚠️ 组件销毁时未找到保存的 window 引用'); + } + }; + }, []); // 加载中状态 if (loading) { @@ -185,7 +291,7 @@ export const CollaboraViewer = forwardRef Window | null; + /** 清除所有高亮 */ + clearAllHighlights: () => Promise; } diff --git a/app/components/reviews/FilePreview.tsx b/app/components/reviews/FilePreview.tsx index 6c9a719..cbd1ca3 100644 --- a/app/components/reviews/FilePreview.tsx +++ b/app/components/reviews/FilePreview.tsx @@ -52,7 +52,8 @@ interface FilePreviewProps { reviewPoints?: ReviewPoint[]; // 设为可选 activeReviewPointResultId: string | null; targetPage?: number; // 新增目标页码参数 - charPositions?: Array<{ box: number[][], char: string, score: number }>; // 字符位置信息 + charPositions?: Array<{ box: number[][], char: string, score: number }>; // 字符位置信息(仅用于PDF) + highlightValue?: string; // 高亮文本值(用于DOCX) isStructuredView?: boolean; // 是否显示结构化视图 userInfo?: { sub: string; @@ -61,7 +62,7 @@ interface FilePreviewProps { } // export function FilePreview({ fileContent, reviewPoints, activeReviewPointResultId, targetPage }: FilePreviewProps) { -export function FilePreview({ fileContent, activeReviewPointResultId, targetPage, charPositions, isStructuredView = false, userInfo }: FilePreviewProps) { +export function FilePreview({ fileContent, activeReviewPointResultId, targetPage, charPositions, highlightValue, isStructuredView = false, userInfo }: FilePreviewProps) { // 获取文件类型 const real_path = fileContent.path || fileContent.template_contract_path || ''; const fileExtension = real_path.split('.').pop()?.toLowerCase(); @@ -73,16 +74,51 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage const contentRef = useRef(null); const collaboraViewerRef = useRef(null); const prevTargetPageRef = useRef(undefined); - const lastMousePosRef = useRef({ x: 0, y: 0 }); // States const [numPages, setNumPages] = useState(null); const [pageInputValue, setPageInputValue] = useState(''); - const [dragMode, setDragMode] = useState(false); - const [isDragging, setIsDragging] = useState(false); - const [dragCursor, setDragCursor] = useState('default'); + const [isDocumentLoading, setIsDocumentLoading] = useState(true); // 文档加载状态 + const [isScrollingToTop, setIsScrollingToTop] = useState(false); // 返回顶部loading状态 + const [isClearingHighlights, setIsClearingHighlights] = useState(false); // 清除高亮loading状态 // ✅ 将所有useEffect移到条件return之前 + // 清除高亮:在组件卸载或文档路径变化时 + useEffect(() => { + // 返回清理函数 + return () => { + if (isDocx && collaboraViewerRef.current?.isReady) { + console.log('[FilePreview] 🔥 文档切换,调用 clearAllHighlights'); + // 调用暴露的清除方法 + collaboraViewerRef.current.clearAllHighlights().catch(error => { + console.error('[FilePreview] ✗ 清除高亮失败:', error); + }); + } + }; + }, [real_path, isDocx]); // 当文档路径变化时,清除旧文档的高亮 + + // 监听文档加载状态 + useEffect(() => { + if (!isDocx) { + setIsDocumentLoading(false); // 非DOCX文件直接设为已加载 + return; + } + + // DOCX文件需要等待 Collabora 准备就绪 + setIsDocumentLoading(true); + + const checkInterval = setInterval(() => { + if (collaboraViewerRef.current?.isReady) { + setIsDocumentLoading(false); + clearInterval(checkInterval); + } + }, 200); + + return () => { + clearInterval(checkInterval); + }; + }, [isDocx, real_path]); // 当文档路径变化时重新检测 + // DOCX 页数获取: 使用 requestPageInfo 方法 useEffect(() => { if (!isDocx || isPdf) return; // PDF文件不需要执行 @@ -123,31 +159,6 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage }; }, [isDocx, isPdf]); - // 监听鼠标离开窗口事件 - useEffect(() => { - if (isPdf) return; // PDF不需要拖拽功能 - - const handleMouseLeave = () => { - if (dragMode && isDragging) { - setIsDragging(false); - setDragCursor('grab'); - } - }; - - const handleMouseUp = () => { - if (!dragMode) return; - setIsDragging(false); - setDragCursor('grab'); - }; - - document.addEventListener('mouseleave', handleMouseLeave); - document.addEventListener('mouseup', handleMouseUp as EventListener); - - return () => { - document.removeEventListener('mouseleave', handleMouseLeave); - document.removeEventListener('mouseup', handleMouseUp as EventListener); - }; - }, [isDragging, dragMode, isPdf]); // 处理页面跳转 useEffect(() => { @@ -214,75 +225,6 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage // DOCX 和其他文件类型继续使用原有逻辑 - // 放大文档(仅用于 DOCX) - const handleZoomIn = () => { - if (!collaboraViewerRef.current?.isReady) { - toastService.warning('文档尚未加载完成,请稍候...'); - return; - } - collaboraViewerRef.current?.unoCommands.zoomIn(); - }; - - // 缩小文档(仅用于 DOCX) - const handleZoomOut = () => { - if (!collaboraViewerRef.current?.isReady) { - toastService.warning('文档尚未加载完成,请稍候...'); - return; - } - collaboraViewerRef.current?.unoCommands.zoomOut(); - }; - - // 切换拖拽模式 - const toggleDragMode = () => { - setDragMode(prev => !prev); - setDragCursor(prev => prev === 'default' ? 'grab' : 'default'); - setIsDragging(false); - }; - - // 处理拖拽开始 - const handleMouseDown = (e: React.MouseEvent) => { - if (!dragMode || e.button !== 0) return; // 只在拖拽模式下响应左键点击 - - // 防止选中文本 - e.preventDefault(); - - // 设置拖拽状态 - setIsDragging(true); - setDragCursor('grabbing'); - - // 记录鼠标初始位置 - lastMousePosRef.current = { - x: e.clientX, - y: e.clientY - }; - }; - - // 处理拖拽过程 - const handleMouseMove = (e: React.MouseEvent) => { - if (!dragMode || !isDragging || !contentRef.current) return; - - // 计算鼠标移动距离 - const dx = e.clientX - lastMousePosRef.current.x; - const dy = e.clientY - lastMousePosRef.current.y; - - // 更新容器滚动位置 - contentRef.current.scrollLeft -= dx; - contentRef.current.scrollTop -= dy; - - // 更新鼠标位置记录 - lastMousePosRef.current = { - x: e.clientX, - y: e.clientY - }; - }; - - // 处理拖拽结束 - const handleMouseUp = () => { - if (!dragMode) return; - - setIsDragging(false); - setDragCursor('grab'); - }; // 获取评查点对应的样式类 // const getHighlightClass = (status: string) => { @@ -339,12 +281,48 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage }; // 滚动到顶部(仅用于 DOCX) - const handleScrollToTop = () => { + const handleScrollToTop = async () => { if (!collaboraViewerRef.current?.isReady) { toastService.warning('文档尚未加载完成,请稍候...'); return; } - collaboraViewerRef.current?.unoCommands.scrollToTop(); + + setIsScrollingToTop(true); + try { + await collaboraViewerRef.current?.unoCommands.scrollToTop(); + console.log('[FilePreview] 已返回顶部'); + } catch (error) { + console.error('[FilePreview] 返回顶部失败:', error); + toastService.error('返回顶部失败'); + } finally { + // 延迟500ms后重置loading状态,给用户足够的视觉反馈 + setTimeout(() => { + setIsScrollingToTop(false); + }, 500); + } + }; + + // 清除所有高亮(仅用于 DOCX) + const handleClearAllHighlights = async () => { + if (!collaboraViewerRef.current?.isReady) { + toastService.warning('文档尚未加载完成,请稍候...'); + return; + } + + setIsClearingHighlights(true); + try { + await collaboraViewerRef.current.clearAllHighlights(); + console.log('[FilePreview] 已清除所有高亮'); + toastService.success('已清除所有高亮'); + } catch (error) { + console.error('[FilePreview] 清除高亮失败:', error); + toastService.error('清除高亮失败'); + } finally { + // 延迟500ms后重置loading状态 + setTimeout(() => { + setIsClearingHighlights(false); + }, 500); + } }; // 渲染文档内容 @@ -368,6 +346,10 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage // 根据文件类型选择不同的渲染方式 // 注意:PDF 文件已在组件开头使用 PdfPreview 组件提前返回 if (fileExtension === 'docx') { + // 使用 highlightValue 作为高亮文本(用户点击评查点时传递的实际文本值) + // 不再从 charPositions 提取,因为 charPositions 是 PDF 特有的坐标信息 + const highlightText = highlightValue; + // DOCX文件使用Collabora Online预览 return ( ); } else { @@ -397,47 +381,66 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage {isStructuredView ? '模板预览' : '文件预览'} -
- - - + {/* 清除高亮按钮 - 仅在DOCX文档时显示 */} + {isDocx && ( + + )} {/* 页码跳转控件 */} -
- + - {numPages && ( @@ -445,66 +448,37 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage )}
- + {/* 缩放提示 - 仅在DOCX文档时显示 */} + {isDocx && ( +
+ + Ctrl+滚轮 +
+ )}
-
- +
); diff --git a/app/components/reviews/ReviewPointsList.tsx b/app/components/reviews/ReviewPointsList.tsx index 8f9813c..1eb953f 100644 --- a/app/components/reviews/ReviewPointsList.tsx +++ b/app/components/reviews/ReviewPointsList.tsx @@ -146,7 +146,7 @@ interface ReviewPointsListProps { reviewPoints: ReviewPoint[]; statistics: Statistics; activeReviewPointResultId: string | null; - onReviewPointSelect: (id: string, page?: number, charPositions?: CharPosition[]) => void; + onReviewPointSelect: (id: string, page?: number, charPositions?: CharPosition[], value?: string) => void; onStatusChange: (id: string, editAuditStatusId: string | number, status: string, message: string) => void; } @@ -1163,15 +1163,15 @@ export function ReviewPointsList({ onClick={(e) => { e.stopPropagation(); if (item.data.page) { - console.log('点击了长链条评查点', item.data.char_positions); + console.log('点击了长链条评查点', item.data.char_positions, item.data); // 假设onReviewPointSelect在作用域内可用 const reviewPointId = reviewPoint.id as string; if (reviewPointId && typeof onReviewPointSelect === 'function') { - onReviewPointSelect(reviewPointId, Number(item.data.page), item.data.char_positions); + onReviewPointSelect(reviewPointId, Number(item.data.page), item.data.char_positions, item.data.value); } } else if(reviewPoint.contentPage && reviewPoint.contentPage[item.field]){ - onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[item.field])); + onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[item.field]), item.data.char_positions, item.data.value); } else{ toastService.error(`没有找到${item.field}对应的索引内容`); @@ -1248,14 +1248,14 @@ export function ReviewPointsList({ onClick={(e) => { e.stopPropagation(); if (chain[0].data.page) { - console.log('点击了短链1左', chain[0].data.char_positions) + console.log('点击了短链1左', chain[0].data.char_positions, chain[0].data) const reviewPointId = reviewPoint.id as string; if (reviewPointId && typeof onReviewPointSelect === 'function') { - onReviewPointSelect(reviewPointId, chain[0].data.page, chain[0].data.char_positions); + onReviewPointSelect(reviewPointId, chain[0].data.page, chain[0].data.char_positions, chain[0].data.value); } } else if(reviewPoint.contentPage && reviewPoint.contentPage[chain[0].field]){ - onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[chain[0].field])); + onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[chain[0].field]), chain[0].data.char_positions,chain[0].data.value); } else{ toastService.error(`没有找到${chain[0].field}对应的索引内容`); @@ -1275,14 +1275,14 @@ export function ReviewPointsList({ onClick={(e) => { e.stopPropagation(); if (chain[1].data.page) { - console.log('点击了短链2右', chain[1].data.char_positions) + console.log('点击了短链2右', chain[1].data.char_positions, chain[1].data) const reviewPointId = reviewPoint.id as string; if (reviewPointId && typeof onReviewPointSelect === 'function') { - onReviewPointSelect(reviewPointId, chain[1].data.page, chain[1].data.char_positions); + onReviewPointSelect(reviewPointId, chain[1].data.page, chain[1].data.char_positions, chain[1].data.value); } } else if(reviewPoint.contentPage && reviewPoint.contentPage[chain[1].field]){ - onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[chain[1].field])); + onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[chain[1].field]), chain[1].data.char_positions, chain[1].data.value); } else{ toastService.error(`没有找到${chain[1].field}对应的索引内容`); @@ -1419,9 +1419,9 @@ export function ReviewPointsList({ e.stopPropagation(); if (mainTypeValue.page && typeof onReviewPointSelect === 'function') { console.log("点击了其他评查点", mainTypeValue) - onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), mainTypeValue.char_positions); + onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), mainTypeValue.char_positions, mainTypeValue.value); }else if(reviewPoint.contentPage && reviewPoint.contentPage[fieldKey]){ - onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[fieldKey])); + onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[fieldKey]), mainTypeValue.char_positions, mainTypeValue.value); }else{ toastService.error(`没有找到${fieldKey}对应的索引内容`); } @@ -1430,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), mainTypeValue.char_positions); + onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), mainTypeValue.char_positions, mainTypeValue.value); }else{ toastService.error(`没有找到${fieldKey}对应的索引内容`); } @@ -1541,10 +1541,10 @@ export function ReviewPointsList({ onClick={(e) => { e.stopPropagation(); if (value.page && typeof onReviewPointSelect === 'function') { - console.log("点击了大模型的评查点", value.char_positions) - onReviewPointSelect(reviewPoint.id, Number(value.page), value.char_positions); + console.log("点击了大模型的评查点", value.char_positions, value) + onReviewPointSelect(reviewPoint.id, Number(value.page), value.char_positions, value.value); }else if(reviewPoint.contentPage && reviewPoint.contentPage[key]){ - onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key])); + onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]), value.char_positions,value.value); }else{ toastService.error(`没有找到${key}对应的索引内容`); } @@ -1554,9 +1554,9 @@ export function ReviewPointsList({ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if (value.page && typeof onReviewPointSelect === 'function') { - onReviewPointSelect(reviewPoint.id, Number(value.page), value.char_positions); + onReviewPointSelect(reviewPoint.id, Number(value.page), value.char_positions, value.value); }else if(reviewPoint.contentPage && reviewPoint.contentPage[key]){ - onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key])); + onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]), value.char_positions,value.value); }else{ toastService.error(`没有找到${key}对应的索引内容`); } diff --git a/app/components/reviews/ReviewTabs.tsx b/app/components/reviews/ReviewTabs.tsx index 670f9b8..5642703 100644 --- a/app/components/reviews/ReviewTabs.tsx +++ b/app/components/reviews/ReviewTabs.tsx @@ -221,6 +221,7 @@ export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfi
{/* 评查结果、AI智能分析、文件信息 */}
+ {/* {JSON.stringify(fileInfo)} */} */} - {fileInfo.type === '1' && ( + {fileInfo.type?.toString().includes('1') && ( + + +
+ + {/* Right side: Info banner */} +
+ +
+ 差异高亮说明: + + 左侧红色:原始版本 | + 右侧绿色:修改版本 | + 深色高亮:字符差异 + +
+
+
+ + {/* Diff Editor main area */} +
+ {/* 只有当两个文本都加载完成后才渲染 Monaco Editor */} + {originalText && modifiedText && !isLoadingDoc1 && !isLoadingDoc2 ? ( + + ) : ( +
+ {isLoadingDoc1 || isLoadingDoc2 ? ( + 正在加载文档... + ) : ( + 等待文档加载... + )} +
+ )} + + {/* Loading overlay */} + {(isLoadingDoc1 || isLoadingDoc2) && ( +
+
+
+
+ 正在加载文档并提取文本... +
+ {isLoadingDoc1 &&
📄 加载原始文档
} + {isLoadingDoc2 &&
📄 加载对比文档
} +
+
+ )} +
+ + {/* Spinner animation */} + + + ); +} diff --git a/app/routes/cross-checking.upload.tsx b/app/routes/cross-checking.upload.tsx index b8e5324..c3bf32e 100644 --- a/app/routes/cross-checking.upload.tsx +++ b/app/routes/cross-checking.upload.tsx @@ -16,6 +16,10 @@ import { formatFileSize, batchUploadAndAssignCrossCheckingFiles } from "~/api/cross-checking/cross-files-upload"; +import { + getCrossCheckingDocumentTypes, + type DocumentType +} from "~/api/cross-checking/cross-files"; import { getOrganizationTree, convertToTreeData @@ -125,16 +129,21 @@ const TreeNodeCheckbox: React.FC<{ ); }; /** - * 获取用户会话和前端JWT + * 获取用户会话和前端JWT,以及文档类型列表 */ export const loader = async ({ request }: LoaderFunctionArgs) => { // 获取用户会话信息 const { getUserSession } = await import("~/api/login/auth.server"); const { userInfo, frontendJWT } = await getUserSession(request); - + + // 获取可用于交叉评查的文档类型列表 + const documentTypesResponse = await getCrossCheckingDocumentTypes(frontendJWT); + return Response.json({ userInfo, - frontendJWT + frontendJWT, + documentTypes: documentTypesResponse.success ? documentTypesResponse.data : [], + documentTypesError: documentTypesResponse.error }); }; @@ -194,10 +203,12 @@ export const action = async ({ request }: ActionFunctionArgs) => { export default function CrossCheckingUpload() { // 获取loader数据 - const { userInfo, frontendJWT } = useLoaderData(); - - // 基础状态 - const [caseType, setCaseType] = useState(CaseType.ADMINISTRATIVE_PENALTY); + const { userInfo, frontendJWT, documentTypes, documentTypesError } = useLoaderData(); + + // 基础状态 - 使用第一个文档类型的ID作为默认值 + const [selectedDocTypeId, setSelectedDocTypeId] = useState( + documentTypes && documentTypes.length > 0 ? documentTypes[0].id : null + ); // 步骤状态 const [currentStep, setCurrentStep] = useState(1); // 任务创建状态 @@ -235,16 +246,17 @@ export default function CrossCheckingUpload() { // 处理案卷类型切换 - const handleCaseTypeChange = (type: CaseType) => { + const handleDocTypeChange = (docTypeId: number) => { if (isUploading) { toastService.warning("上传进行中,无法切换案卷类型"); return; } - setCaseType(type); + setSelectedDocTypeId(docTypeId); // 清空已选择的文件和重置上传方式 clearAllFiles(); - console.log("案卷类型切换为:", type, "typeId:", CASE_TYPE_TO_TYPE_ID[type]); + const selectedType = documentTypes?.find((dt: DocumentType) => dt.id === docTypeId); + console.log("案卷类型切换为:", selectedType?.name, "ID:", docTypeId); }; // 清空所有文件 @@ -268,7 +280,11 @@ export default function CrossCheckingUpload() { let hasInvalidFiles = false; Array.from(files).forEach(file => { - if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) { + const isPdf = file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf'); + const isDocx = file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || + file.name.toLowerCase().endsWith('.docx'); + + if (isPdf || isDocx) { validFiles.push({ id: generateFileId(), file, @@ -283,7 +299,7 @@ export default function CrossCheckingUpload() { }); if (hasInvalidFiles) { - messageService.error('只能上传PDF格式的文件', { + messageService.error('只能上传PDF或DOCX格式的文件', { title: '文件类型错误', confirmText: '确定', }); @@ -413,12 +429,25 @@ export default function CrossCheckingUpload() { return; } + // 验证选择了案卷类型 + if (!selectedDocTypeId) { + toastService.error("请选择案卷类型"); + return; + } + setIsCreatingTask(true); setIsUploading(true); - + try { + // 获取选中的文档类型信息 + const selectedDocType = documentTypes?.find((dt: DocumentType) => dt.id === selectedDocTypeId); + if (!selectedDocType) { + toastService.error("无效的案卷类型"); + return; + } + // 第一步:上传文件并自动分配任务(新接口) - console.log("开始批量上传文件并分配任务:", filesToUpload.length, "个,案卷类型:", caseType); + console.log("开始批量上传文件并分配任务:", filesToUpload.length, "个,案卷类型:", selectedDocType.name); // 提取用户ID(从选中的组织架构中获取用户) const userIds = groupChecked.filter(id => { @@ -431,22 +460,17 @@ export default function CrossCheckingUpload() { return; } - // 创建任务数据 - const docTypeMap = { - [CaseType.ADMINISTRATIVE_PENALTY]: 'XZCF', - [CaseType.ADMINISTRATIVE_PERMIT]: 'XZXK' - }; - + // 使用文档类型名称作为 doc_type const uploadResult = await batchUploadAndAssignCrossCheckingFiles( filesToUpload, - CASE_TYPE_TO_TYPE_ID[caseType], + selectedDocTypeId, // 使用选中的文档类型ID priority, documentNumber, remark, isTestDocument, userIds, taskInfo.name, - docTypeMap[caseType] || 'XZCF', + selectedDocType.name, // 使用文档类型名称 frontendJWT ); @@ -814,29 +838,36 @@ export default function CrossCheckingUpload() {
选择案卷类型
-
- - -
+ {documentTypesError ? ( +
+ + 加载案卷类型失败: {documentTypesError} +
+ ) : documentTypes && documentTypes.length > 0 ? ( +
+ {documentTypes.map((docType: DocumentType) => ( + + ))} +
+ ) : ( +
+ + 暂无可用的案卷类型 +
+ )}
- + {/* 文件上传区域 */} - + {/* 上传框区域 */} @@ -851,14 +882,14 @@ export default function CrossCheckingUpload() { ref={singleUploadRef} onFilesSelected={handleSingleFilesSelected} className="custom-upload-area" - accept=".pdf" + accept=".pdf,.docx" multiple={true} icon="ri-file-upload-line" buttonText="选择文件" mainText="点击或拖拽文件到此区域上传" tipText={
- 请上传案件相关PDF文件 + 请上传案件相关PDF或DOCX文件
} disabled={uploadType === 'multiple' || isUploading} @@ -911,23 +942,29 @@ export default function CrossCheckingUpload() { {/* 单案件文件列表 */} {uploadType === 'single' && singleFiles.length > 0 && (
- {singleFiles.map((file) => ( -
-
- - {file.name} - {formatFileSize(file.size)} + {singleFiles.map((file) => { + const isDocx = file.name.toLowerCase().endsWith('.docx'); + const isPdf = file.name.toLowerCase().endsWith('.pdf'); + return ( +
+
+ {isPdf && } + {isDocx && } + {!isPdf && !isDocx && } + {file.name} + {formatFileSize(file.size)} +
+
- -
- ))} + ); + })}
)} diff --git a/app/routes/documents.list.tsx b/app/routes/documents.list.tsx index fa08830..077f13c 100644 --- a/app/routes/documents.list.tsx +++ b/app/routes/documents.list.tsx @@ -1549,8 +1549,19 @@ export default function DocumentsIndex() { {/* 附件追加模态框 */} {showAttachmentUpload && ( -
-
+
{ + setShowAttachmentUpload(false); + setSelectedDocumentId(null); + setAttachmentFiles([]); + setAttachmentRemark(""); + }} + > +
e.stopPropagation()} + >

追加合同附件