feat: 1. 接入CollaboraViewer选中的高亮效果,清除高亮功能,页面销毁自动清除高亮。

2. 合同模板对比接入monaco editor的效果。
3. 添加交叉评查的案卷类型的数据查询。

fix: 1. 修复文档列表的打开模态框蒙板层显示效果。
This commit is contained in:
2025-11-30 19:33:05 +08:00
parent fb67f138dc
commit 4fcc92a381
14 changed files with 1263 additions and 286 deletions
+97 -60
View File
@@ -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<typeof loader>();
// 基础状态
const [caseType, setCaseType] = useState<CaseType>(CaseType.ADMINISTRATIVE_PENALTY);
const { userInfo, frontendJWT, documentTypes, documentTypesError } = useLoaderData<typeof loader>();
// 基础状态 - 使用第一个文档类型的ID作为默认值
const [selectedDocTypeId, setSelectedDocTypeId] = useState<number | null>(
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() {
<div className="flex justify-center mb-6">
<div>
<div className="text-sm font-medium text-gray-700 mb-3 text-center"></div>
<div className="case-type-options">
<button
type="button"
className={`case-type-option ${caseType === CaseType.ADMINISTRATIVE_PENALTY ? 'active' : 'inactive'}`}
onClick={() => handleCaseTypeChange(CaseType.ADMINISTRATIVE_PENALTY)}
disabled={isUploading}
>
</button>
<button
type="button"
className={`case-type-option ${caseType === CaseType.ADMINISTRATIVE_PERMIT ? 'active' : 'inactive'}`}
onClick={() => handleCaseTypeChange(CaseType.ADMINISTRATIVE_PERMIT)}
disabled={isUploading}
>
</button>
</div>
{documentTypesError ? (
<div className="text-red-500 text-sm text-center p-4 border border-red-200 rounded-md bg-red-50">
<i className="ri-error-warning-line mr-2"></i>
: {documentTypesError}
</div>
) : documentTypes && documentTypes.length > 0 ? (
<div className="case-type-options">
{documentTypes.map((docType: DocumentType) => (
<button
key={docType.id}
type="button"
className={`case-type-option ${selectedDocTypeId === docType.id ? 'active' : 'inactive'}`}
onClick={() => handleDocTypeChange(docType.id)}
disabled={isUploading}
>
{docType.name}
</button>
))}
</div>
) : (
<div className="text-gray-500 text-sm text-center p-4 border border-gray-200 rounded-md bg-gray-50">
<i className="ri-information-line mr-2"></i>
</div>
)}
</div>
</div>
{/* 文件上传区域 */}
<input type="hidden" name="caseType" value={caseType} />
<input type="hidden" name="selectedDocTypeId" value={selectedDocTypeId || ''} />
<input type="hidden" name="uploadType" value={uploadType} />
{/* 上传框区域 */}
@@ -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={
<div className="upload-tip-error">
PDF文件
PDF或DOCX文件
</div>
}
disabled={uploadType === 'multiple' || isUploading}
@@ -911,23 +942,29 @@ export default function CrossCheckingUpload() {
{/* 单案件文件列表 */}
{uploadType === 'single' && singleFiles.length > 0 && (
<div className="max-h-32 overflow-y-auto space-y-1">
{singleFiles.map((file) => (
<div key={file.id} className="flex items-center justify-between bg-white p-2 rounded border">
<div className="flex items-center space-x-2 flex-1 min-w-0">
<i className="ri-file-pdf-line text-red-500"></i>
<span className="text-sm truncate">{file.name}</span>
<span className="text-xs text-gray-500">{formatFileSize(file.size)}</span>
{singleFiles.map((file) => {
const isDocx = file.name.toLowerCase().endsWith('.docx');
const isPdf = file.name.toLowerCase().endsWith('.pdf');
return (
<div key={file.id} className="flex items-center justify-between bg-white p-2 rounded border">
<div className="flex items-center space-x-2 flex-1 min-w-0">
{isPdf && <i className="ri-file-pdf-line text-red-500"></i>}
{isDocx && <i className="ri-file-word-2-line text-blue-500"></i>}
{!isPdf && !isDocx && <i className="ri-file-line text-gray-500"></i>}
<span className="text-sm truncate">{file.name}</span>
<span className="text-xs text-gray-500">{formatFileSize(file.size)}</span>
</div>
<button
type="button"
onClick={() => handleRemoveFile(file.id, 'single')}
className="text-red-500 hover:text-red-700 p-1"
disabled={isUploading}
>
<i className="ri-close-line"></i>
</button>
</div>
<button
type="button"
onClick={() => handleRemoveFile(file.id, 'single')}
className="text-red-500 hover:text-red-700 p-1"
disabled={isUploading}
>
<i className="ri-close-line"></i>
</button>
</div>
))}
);
})}
</div>
)}
+25 -4
View File
@@ -1549,8 +1549,19 @@ export default function DocumentsIndex() {
{/* 附件追加模态框 */}
{showAttachmentUpload && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto">
<div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999]"
onClick={() => {
setShowAttachmentUpload(false);
setSelectedDocumentId(null);
setAttachmentFiles([]);
setAttachmentRemark("");
}}
>
<div
className="bg-white rounded-lg p-6 w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold"></h3>
<button
@@ -1688,8 +1699,18 @@ export default function DocumentsIndex() {
{/* 合同模板上传模态框 */}
{showTemplateUpload && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 w-full max-w-lg mx-4">
<div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999]"
onClick={() => {
setShowTemplateUpload(false);
setSelectedDocumentId(null);
setTemplateFile(null);
}}
>
<div
className="bg-white rounded-lg p-6 w-full max-w-lg mx-4"
onClick={(e) => e.stopPropagation()}
>
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold"></h3>
<button
+1 -1
View File
@@ -276,7 +276,7 @@ export default function EntryModulesList() {
rel="noopener noreferrer"
className="ml-2 text-blue-600 hover:underline text-sm"
>
<div className="h-8 w-8 bg-gray-100 rounded flex items-center justify-center overflow-hidden">
<div className="h-10 w-10 bg-gray-100 rounded flex items-center justify-center overflow-hidden">
<img
src={logoUrl}
alt={record.name}
+47 -15
View File
@@ -43,6 +43,9 @@ import {
Comparison
} from "~/components/reviews";
// 导入文档对比组件
import { ComparePreview } from "~/components/reviews/previewComponents/ComparePreview";
// 从ReviewPointsList组件中导入ReviewPoint类型
import { type ReviewPoint } from '~/components/reviews';
import { messageService } from "~/components/ui/MessageModal";
@@ -306,6 +309,7 @@ export default function ReviewDetails() {
const [targetPage, setTargetPage] = useState<number | undefined>(undefined);
const [templateTargetPage, setTemplateTargetPage] = useState<number | undefined>(undefined);
const [charPositions, setCharPositions] = useState<Array<{ box: number[][], char: string, score: number }> | undefined>(undefined);
const [highlightValue, setHighlightValue] = useState<string | undefined>(undefined);
const [pendingUpdate, setPendingUpdate] = useState<{
reviewPointResultId: string;
newStatus: string;
@@ -352,10 +356,22 @@ export default function ReviewDetails() {
},[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 || "未知文件名",
@@ -395,22 +411,25 @@ export default function ReviewDetails() {
setActiveTab(tabKey);
};
const handleReviewPointSelect = (reviewPointId: string, page?: number, charPos?: Array<{ box: number[][], char: string, score: number }>) => {
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);
// 使用setTimeout确保状态更新后再设置新的targetPage和charPositions
setHighlightValue(undefined);
// 使用setTimeout确保状态更新后再设置新的targetPage、charPositions和highlightValue
setTimeout(() => {
setActiveReviewPointResultId(reviewPointId);
setTargetPage(page);
setCharPositions(charPos);
setHighlightValue(value);
}, 0);
} else {
// 正常设置activeReviewPointId、targetPagecharPositions
// 正常设置activeReviewPointId、targetPagecharPositions和highlightValue
setActiveReviewPointResultId(reviewPointId);
setTargetPage(page);
setCharPositions(charPos);
setHighlightValue(value);
}
};
@@ -733,7 +752,7 @@ export default function ReviewDetails() {
previousRoute: loaderData.previousRoute,
path: document?.path,
auditStatus: document?.auditStatus,
type: document?.type,
type: document?.type || document?.type_id,
comparisonId: comparison_document?.id ? Number(comparison_document.id) : undefined
}}
onConfirmResults={handleConfirmResults}
@@ -742,6 +761,7 @@ export default function ReviewDetails() {
{/* 评查结果选项卡内容 */}
{activeTab === 'preview' && (
<div className="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:space-x-4">
{/* {JSON.stringify(document)} */}
{/* 左侧:文件预览 */}
<div className="w-full lg:w-[65%]">
{(() => {
@@ -759,6 +779,8 @@ export default function ReviewDetails() {
activeReviewPointResultId={activeReviewPointResultId}
targetPage={targetPage}
charPositions={charPositions}
highlightValue={highlightValue}
userInfo={loaderData.userInfo}
/>
);
})()}
@@ -779,8 +801,23 @@ export default function ReviewDetails() {
{/* 结构比对选项卡内容 */}
{activeTab === 'filecompare' && (
<div className="w-full" style={{
height: 'calc(100vh - 120px)',
minHeight: '600px',
display: 'flex',
flexDirection: 'column'
}}>
{/* {JSON.stringify(comparison_document?.template_contract_path)} -----{JSON.stringify(document?.path)} */}
<ComparePreview
doc1Path={document?.path || ''}
doc2Path={comparison_document?.template_contract_path || ''}
/>
</div>
)}
{/* 原来的结构比对选项卡内容(已注释) */}
{/* {activeTab === 'filecompare' && (
<div className="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:space-x-4">
{/* 左侧:原文件预览 */}
<div className={`w-full ${comparison_document.template_contract_path ? 'lg:w-[38%]' : 'lg:w-[56%]'}`}>
<FilePreview
fileContent={document}
@@ -790,10 +827,9 @@ export default function ReviewDetails() {
charPositions={charPositions}
/>
</div>
{/* 中间:附件文件预览 */}
<div className={`w-full ${comparison_document.template_contract_path ? 'lg:w-[38%]' : 'lg:w-[20%]'}`}>
<FilePreview
<FilePreview
fileContent={comparison_document}
reviewPoints={[]}
activeReviewPointResultId={activeReviewPointResultId}
@@ -801,15 +837,12 @@ export default function ReviewDetails() {
isStructuredView={true}
/>
</div>
{/* 右侧:结构比较结果 */}
<div className="w-full lg:w-[24%]">
<Comparison
comparison_document={comparison_document}
onPageJump={(sourcePage, templatePage) => {
// 同时处理主文件和模板文件的页码跳转
if (sourcePage > 0) {
// 如果目标页码与当前页码相同,先重置再设置以强制触发更新
if (sourcePage === targetPage) {
setTargetPage(undefined);
setTimeout(() => setTargetPage(sourcePage), 0);
@@ -819,7 +852,6 @@ export default function ReviewDetails() {
console.log(`跳转到主文件第${sourcePage}页`);
}
if (templatePage > 0) {
// 如果目标页码与当前页码相同,先重置再设置以强制触发更新
if (templatePage === templateTargetPage) {
setTemplateTargetPage(undefined);
setTimeout(() => setTemplateTargetPage(templatePage), 0);
@@ -832,7 +864,7 @@ export default function ReviewDetails() {
/>
</div>
</div>
)}
)} */}
{/* AI智能分析选项卡内容 */}
{activeTab === 'analysis' && (