feat: 1. 接入CollaboraViewer选中的高亮效果,清除高亮功能,页面销毁自动清除高亮。
2. 合同模板对比接入monaco editor的效果。 3. 添加交叉评查的案卷类型的数据查询。 fix: 1. 修复文档列表的打开模态框蒙板层显示效果。
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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、targetPage和charPositions
|
||||
// 正常设置activeReviewPointId、targetPage、charPositions和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' && (
|
||||
|
||||
Reference in New Issue
Block a user