feat: migrate cross checking ui to v3 flow
This commit is contained in:
@@ -25,24 +25,26 @@
|
||||
import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/node";
|
||||
import React, { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
||||
import { useNavigate, useLoaderData } from "@remix-run/react";
|
||||
import crossCheckingStyles from "~/styles/cross-checking-result.css?url";
|
||||
import { getReviewPoints, updateReviewResult, getReviewPoints_fromApi} from "~/api/evaluation_points/reviews";
|
||||
import reviewsStyles from "~/styles/reviews.css?url";
|
||||
import { updateReviewResult, getReviewPoints_fromApi, getUnifiedEvaluationResults } from "~/api/evaluation_points/reviews";
|
||||
import { postgrestGet } from "~/api/postgrest-client";
|
||||
import { toastService } from "~/components/ui/Toast";
|
||||
import { confirmReviewResults, checkProposalVotes, findIsProposer } from "~/api/cross-checking/cross-file-result";
|
||||
import { usePermission } from "~/hooks/usePermission";
|
||||
|
||||
// 导入交叉评查详情页面组件
|
||||
// 复用新版评查详情页外壳,保留交叉评查提案面板
|
||||
import {
|
||||
FileInfo,
|
||||
ReviewTabs,
|
||||
FilePreview,
|
||||
ReviewPointsList
|
||||
} from "~/components/cross-checking";
|
||||
FileDetails,
|
||||
} from "~/components/reviews";
|
||||
import { ReviewPointsList, type CharPosition } from "~/components/cross-checking";
|
||||
|
||||
// 导入文档对比组件
|
||||
import { ComparePreview } from "~/components/reviews/previewComponents/ComparePreview";
|
||||
|
||||
// 从ReviewPointsList组件中导入ReviewPoint类型和CharPosition类型
|
||||
import { type ReviewPoint, type CharPosition } from '~/components/cross-checking';
|
||||
// 从ReviewPointsList组件中导入ReviewPoint类型
|
||||
import { type ReviewPoint } from '~/components/cross-checking';
|
||||
import { messageService } from "~/components/ui/MessageModal";
|
||||
import { loadingBarService } from "~/components/ui/LoadingBar";
|
||||
import { Breadcrumb } from "~/components/layout/Breadcrumb";
|
||||
@@ -177,7 +179,7 @@ export const meta: MetaFunction = () => {
|
||||
};
|
||||
|
||||
export function links() {
|
||||
return [{ rel: "stylesheet", href: crossCheckingStyles }];
|
||||
return [{ rel: "stylesheet", href: reviewsStyles }];
|
||||
}
|
||||
|
||||
export const handle = {
|
||||
@@ -229,15 +231,102 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
|
||||
// console.log(`✅ [Loader] 用户 ${userInfo.user_id} (${accessCheck.userRole}) 访问文档 ${id} - 权限验证通过`);
|
||||
|
||||
async function patchPointCodes(points: Array<Record<string, any>>, jwt: string) {
|
||||
try {
|
||||
const pointIds = points.map((point) => point.pointId).filter(Boolean);
|
||||
if (pointIds.length === 0) return;
|
||||
|
||||
const response = await postgrestGet<any>('/api/postgrest/proxy/evaluation_points', {
|
||||
select: 'id,code',
|
||||
filter: { id: `in.(${[...new Set(pointIds)].join(',')})` },
|
||||
token: jwt,
|
||||
});
|
||||
|
||||
const raw = response.data;
|
||||
const pointList = Array.isArray(raw) ? raw : (raw?.data && Array.isArray(raw.data) ? raw.data : []);
|
||||
const codeMap: Record<string, string> = {};
|
||||
|
||||
pointList.forEach((point: any) => {
|
||||
if (point.code) {
|
||||
codeMap[String(point.id)] = point.code;
|
||||
}
|
||||
});
|
||||
|
||||
points.forEach((point) => {
|
||||
point.pointCode = codeMap[String(point.pointId)] || point.pointCode || '';
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[CrossChecking Loader] patchPointCodes error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const isProposer = await findIsProposer(taskId, userInfo?.user_id, frontendJWT);
|
||||
const unifiedData = await getUnifiedEvaluationResults(id, request);
|
||||
|
||||
if (!('error' in unifiedData) && unifiedData.flow_type === 'graphrag') {
|
||||
const reviewData = await getReviewPoints_fromApi(id, request);
|
||||
const existingPoints = ('data' in reviewData && !('error' in reviewData)) ? reviewData.data : [];
|
||||
const notApplicablePoints = (unifiedData.results || [])
|
||||
.filter((result: any) => result.result_type === 'not_applicable')
|
||||
.map((result: any) => ({
|
||||
id: `na-${result.evaluation_point_id}`,
|
||||
documentId: id,
|
||||
pointId: result.evaluation_point_id,
|
||||
editAuditStatusId: '',
|
||||
editAuditStatus: '',
|
||||
editAuditStatusMessage: '',
|
||||
title: '该评查点未涉及',
|
||||
pointName: result.name || '',
|
||||
pointCode: result.code || '',
|
||||
groupName: '',
|
||||
status: 'notApplicable',
|
||||
content: {},
|
||||
contentPage: {},
|
||||
suggestion: result.ai_suggestion || '该评查点未涉及',
|
||||
result: null,
|
||||
score: result.score || 0,
|
||||
finalScore: null,
|
||||
machineScore: 0,
|
||||
postAction: '',
|
||||
}));
|
||||
const allReviewPoints = [...existingPoints, ...notApplicablePoints];
|
||||
await patchPointCodes(allReviewPoints as Array<Record<string, any>>, frontendJWT);
|
||||
|
||||
return Response.json({
|
||||
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,
|
||||
score: unifiedData.summary?.total_score || 0
|
||||
},
|
||||
comparison_document: ('comparison_document' in reviewData && !('error' in reviewData)) ? reviewData.comparison_document : null,
|
||||
scoring_proposals: ('scoring_proposals' in reviewData && !('error' in reviewData)) ? (reviewData.scoring_proposals || []) : [],
|
||||
userInfo,
|
||||
jwtToken: frontendJWT,
|
||||
isProposer,
|
||||
taskId,
|
||||
taskName,
|
||||
flowType: 'graphrag'
|
||||
});
|
||||
}
|
||||
|
||||
// 对接接口,新的获取评查点结果的方法
|
||||
const reviewData = await getReviewPoints_fromApi(id, request)
|
||||
const reviewData = await getReviewPoints_fromApi(id, request);
|
||||
|
||||
// 获取评查点数据,传递request对象 旧获取评查点结果的方法
|
||||
// const reviewData = await getReviewPoints(id, request);
|
||||
|
||||
// 获取当前登录用户是否是负责人
|
||||
const isProposer = await findIsProposer(taskId, userInfo?.user_id, frontendJWT);
|
||||
|
||||
// console.log("documentData-------",JSON.stringify(documentData.data,null,2));
|
||||
// console.log("reviewData-------",JSON.stringify('data' in reviewData ? reviewData.data : '',null,2));
|
||||
// console.log("reviewData-------",JSON.stringify(reviewData,null,2));
|
||||
@@ -249,6 +338,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
|
||||
// 确保reviewData有效且具有预期的属性
|
||||
if ('document' in reviewData && 'data' in reviewData && 'reviewInfo' in reviewData && 'stats' in reviewData) {
|
||||
await patchPointCodes(reviewData.data as Array<Record<string, any>>, frontendJWT);
|
||||
// console.log("reviewData-------",JSON.stringify(reviewData.data));
|
||||
return Response.json({
|
||||
previousRoute: previousRoute,
|
||||
@@ -262,7 +352,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
jwtToken: frontendJWT, // 传递JWT token
|
||||
isProposer: isProposer,
|
||||
taskId: taskId, // 传递任务ID
|
||||
taskName: taskName // 传递任务名称
|
||||
taskName: taskName, // 传递任务名称
|
||||
flowType: 'legacy'
|
||||
});
|
||||
} else {
|
||||
console.error("返回的评查数据格式不正确",JSON.stringify(reviewData,null,2));
|
||||
@@ -338,12 +429,7 @@ export default function CrossCheckingResult() {
|
||||
const { document, reviewPoints, statistics, reviewInfo, comparison_document, scoring_proposals, jwtToken, userInfo, isProposer, taskId, taskName } = loaderData;
|
||||
const [isLoading, setIsLoading] = useState(false); // 已经通过loader加载了数据,不需要再显示加载状态
|
||||
|
||||
// 视图切换状态:'review' = 评查结果视图, 'compare' = 结构比对视图
|
||||
const [viewMode, setViewMode] = useState<'review' | 'compare'>('review');
|
||||
|
||||
|
||||
// 判断是否有模板可以进行结构比对
|
||||
const hasTemplateForCompare = Boolean(comparison_document?.template_contract_path?.trim());
|
||||
const [activeTab, setActiveTab] = useState<string>('preview');
|
||||
|
||||
// 权限控制
|
||||
const { hasPermission } = usePermission();
|
||||
@@ -377,6 +463,12 @@ export default function CrossCheckingResult() {
|
||||
setLocalScoringProposals(scoring_proposals || []);
|
||||
}, [scoring_proposals]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!comparison_document?.template_contract_path?.trim() && activeTab === 'filecompare') {
|
||||
setActiveTab('preview');
|
||||
}
|
||||
}, [activeTab, comparison_document?.template_contract_path]);
|
||||
|
||||
// 处理意见提交成功的回调
|
||||
const handleOpinionSubmitted = useCallback((newProposal: ScoringProposal) => {
|
||||
setLocalScoringProposals(prev => [...prev, newProposal]);
|
||||
@@ -697,7 +789,7 @@ export default function CrossCheckingResult() {
|
||||
isProcessingRef.current = false;
|
||||
toastService.error(`确认评查结果失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
}
|
||||
}, [document, jwtToken, navigate]);
|
||||
}, [document, jwtToken, navigate, taskId, taskName]);
|
||||
|
||||
// 构建自定义面包屑项 - 使用 useMemo 缓存
|
||||
const breadcrumbItems = useMemo(() => {
|
||||
@@ -716,7 +808,7 @@ export default function CrossCheckingResult() {
|
||||
}, [document?.id, loaderData.previousRoute]);
|
||||
|
||||
return (
|
||||
<div className="cross-checking-result-container">
|
||||
<div className="review-container">
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center items-center p-12">
|
||||
<div className="loading-spinner"></div>
|
||||
@@ -753,90 +845,35 @@ export default function CrossCheckingResult() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 返回按钮 */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
// 返回到交叉评查列表页,并带上任务信息以自动打开模态框
|
||||
const params = new URLSearchParams({
|
||||
openModal: 'true',
|
||||
taskId: taskId || '',
|
||||
taskName: taskName || '任务详情'
|
||||
});
|
||||
navigate(`/cross-checking?${params.toString()}`);
|
||||
}}
|
||||
className="inline-flex items-center px-3 py-1.5 text-sm font-medium text-gray-600 border border-gray-400 rounded-md hover:bg-gray-100 hover:text-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-300 mr-3"
|
||||
>
|
||||
<i className="ri-arrow-left-line mr-1.5"></i>
|
||||
返回任务
|
||||
</button>
|
||||
|
||||
{/* 结构比对/查看评查结果按钮 - 仅当文档类型包含"合同"且有模板时显示 */}
|
||||
{hasTemplateForCompare && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setViewMode(viewMode === 'review' ? 'compare' : 'review')}
|
||||
className="inline-flex items-center px-3 py-1.5 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 mr-2"
|
||||
>
|
||||
{viewMode === 'review' ? (
|
||||
<>
|
||||
<i className="ri-file-copy-2-line mr-1.5"></i>
|
||||
结构比对
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<i className="ri-file-list-3-line mr-1.5"></i>
|
||||
查看评查结果
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 完成评查按钮 - 需要 isProposer 且拥有 cross_review:document:complete 权限 */}
|
||||
{isProposer && canCompleteDocument && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
// 立即阻止所有默认行为
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
// 异步调用处理函数
|
||||
void handleConfirmResults(event);
|
||||
}}
|
||||
disabled={isLoading}
|
||||
className="inline-flex items-center px-3 py-1.5 text-sm font-medium text-white bg-green-800 border border-transparent rounded-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-800 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<i className="ri-loader-4-line ri-spin animate-spin mr-1.5"></i>
|
||||
处理中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<i className="ri-check-double-line mr-1.5"></i>
|
||||
完成评查
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 文件信息和操作按钮 */}
|
||||
{/* <FileInfo
|
||||
<ReviewTabs
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
fileInfo={{
|
||||
...reviewData.fileInfo,
|
||||
previousRoute: loaderData.previousRoute
|
||||
id: document?.id,
|
||||
previousRoute: loaderData.previousRoute,
|
||||
path: document?.path,
|
||||
auditStatus: document?.auditStatus,
|
||||
type: document?.type || document?.type_id,
|
||||
comparisonId: comparison_document?.id ? Number(comparison_document.id) : undefined,
|
||||
backTo: `/cross-checking?${new URLSearchParams({
|
||||
openModal: 'true',
|
||||
taskId: taskId || '',
|
||||
taskName: taskName || '任务详情'
|
||||
}).toString()}`
|
||||
}}
|
||||
onConfirmResults={handleConfirmResults}
|
||||
/> */}
|
||||
|
||||
{/* 根据视图模式切换内容 */}
|
||||
{viewMode === 'review' ? (
|
||||
/* 评查结果视图 */
|
||||
<div className="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:space-x-4 lg:justify-between">
|
||||
onConfirmResults={() => {
|
||||
void handleConfirmResults();
|
||||
}}
|
||||
jwtToken={jwtToken}
|
||||
showConfirmButton={Boolean(isProposer && canCompleteDocument)}
|
||||
showCompareTab={Boolean(comparison_document?.template_contract_path?.trim())}
|
||||
>
|
||||
{activeTab === 'preview' && (
|
||||
<div className="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:space-x-4">
|
||||
{/* 左侧:文件预览 */}
|
||||
<div className="w-full lg:w-[62%]">
|
||||
<div className="w-full lg:w-[65%]">
|
||||
<FilePreview
|
||||
fileContent={document}
|
||||
reviewPoints={reviewData.reviewPoints}
|
||||
@@ -845,6 +882,7 @@ export default function CrossCheckingResult() {
|
||||
charPositions={charPositions}
|
||||
highlightValue={highlightValue}
|
||||
aiSuggestionReplace={aiSuggestionReplace}
|
||||
userInfo={loaderData.userInfo}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -868,10 +906,11 @@ export default function CrossCheckingResult() {
|
||||
canVoteProposal={canVoteProposal}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* 结构比对视图 */
|
||||
<div className="w-full" style={{
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'filecompare' && (
|
||||
<div className="w-full" style={{
|
||||
height: 'calc(100vh - 120px)',
|
||||
minHeight: '600px',
|
||||
display: 'flex',
|
||||
@@ -881,8 +920,17 @@ export default function CrossCheckingResult() {
|
||||
doc1Path={document?.path || ''}
|
||||
doc2Path={comparison_document?.template_contract_path || ''}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'fileinfo' && (
|
||||
<FileDetails
|
||||
fileInfo={reviewData.fileInfo}
|
||||
contractInfo={reviewData.contractInfo}
|
||||
reviewInfo={reviewData.reviewInfo}
|
||||
/>
|
||||
)}
|
||||
</ReviewTabs>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
type CrossCheckingUploadedFile,
|
||||
generateFileId,
|
||||
formatFileSize,
|
||||
batchUploadAndAssignCrossCheckingFiles,
|
||||
uploadCrossCheckingDocument,
|
||||
createCrossReviewTask
|
||||
} from "~/api/cross-checking/cross-files-upload";
|
||||
import {
|
||||
@@ -302,8 +302,7 @@ export default function CrossCheckingUpload() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 第一步:上传文件并自动分配任务(新接口)
|
||||
// console.log("开始批量上传文件并分配任务:", filesToUpload.length, "个,案卷类型:", selectedDocType.name);
|
||||
// 第一步:先上传文档到平台,再用 v3 接口创建交叉评查任务
|
||||
|
||||
// 提取用户ID(从选中的组织架构中获取用户)
|
||||
const userIds = groupChecked.filter(id => {
|
||||
@@ -316,23 +315,6 @@ export default function CrossCheckingUpload() {
|
||||
return;
|
||||
}
|
||||
|
||||
// const requireParam = {
|
||||
// filesToUpload: filesToUpload,
|
||||
// selectedDocTypeId: selectedDocTypeId,
|
||||
// priority: priority,
|
||||
// documentNumber: documentNumber,
|
||||
// remark: remark,
|
||||
// isTestDocument: isTestDocument,
|
||||
// userIds: userIds,
|
||||
// taskInfo_name: taskInfo.name,
|
||||
// selectedDocType_name: selectedDocType.code,
|
||||
// taskInfo_type: taskInfo.type,
|
||||
// frontendJWT
|
||||
// }
|
||||
|
||||
// // console.log("requireParam", requireParam)
|
||||
// return;
|
||||
|
||||
// 构建负责人ID数组:当前用户(主要负责人)+ 额外负责人
|
||||
const principalUserIds: number[] = [];
|
||||
// 添加当前用户作为主要负责人
|
||||
@@ -347,37 +329,63 @@ export default function CrossCheckingUpload() {
|
||||
}
|
||||
});
|
||||
|
||||
// 使用文档类型名称作为 doc_type
|
||||
const uploadResult = await batchUploadAndAssignCrossCheckingFiles(
|
||||
filesToUpload,
|
||||
selectedDocTypeId, // 使用选中的文档类型ID
|
||||
priority,
|
||||
documentNumber,
|
||||
remark,
|
||||
isTestDocument,
|
||||
const uploadSuccesses: Array<{ file: CrossCheckingUploadedFile; documentId: number }> = [];
|
||||
const uploadFailures: Array<{ file: CrossCheckingUploadedFile; error: string }> = [];
|
||||
|
||||
for (const fileInfo of filesToUpload) {
|
||||
const binaryData = await fileInfo.file.arrayBuffer();
|
||||
const uploadResponse = await uploadCrossCheckingDocument(
|
||||
binaryData,
|
||||
fileInfo.name,
|
||||
fileInfo.type,
|
||||
selectedDocTypeId,
|
||||
priority,
|
||||
documentNumber,
|
||||
remark,
|
||||
isTestDocument,
|
||||
null,
|
||||
false,
|
||||
frontendJWT
|
||||
);
|
||||
|
||||
if (uploadResponse.error || !uploadResponse.data?.result?.id) {
|
||||
uploadFailures.push({
|
||||
file: fileInfo,
|
||||
error: uploadResponse.error || '上传后未返回文档ID'
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
uploadSuccesses.push({
|
||||
file: fileInfo,
|
||||
documentId: uploadResponse.data.result.id
|
||||
});
|
||||
}
|
||||
|
||||
if (uploadFailures.length > 0) {
|
||||
toastService.error(`文件上传失败:${uploadFailures[0].error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const createTaskResult = await createCrossReviewTask({
|
||||
documentIds: uploadSuccesses.map(item => item.documentId),
|
||||
userIds,
|
||||
taskInfo.name,
|
||||
selectedDocType.code, // 使用文档类型code
|
||||
taskInfo.type, // 使用任务类型(市局间交叉评查 或 区局间交叉评查)
|
||||
frontendJWT,
|
||||
principalUserIds, // 负责人ID数组
|
||||
attributeType // 合同类型
|
||||
);
|
||||
principalUserIds,
|
||||
taskName: taskInfo.name,
|
||||
docTypeId: selectedDocTypeId,
|
||||
docType: selectedDocType.code,
|
||||
taskType: taskInfo.type
|
||||
}, frontendJWT);
|
||||
|
||||
|
||||
// return;
|
||||
|
||||
const { successes, failures } = uploadResult;
|
||||
|
||||
if (failures.length > 0) {
|
||||
toastService.error(`文件上传或任务分配失败:${failures[0].error}`);
|
||||
if (!createTaskResult.success) {
|
||||
toastService.error(createTaskResult.error || '创建交叉评查任务失败');
|
||||
return;
|
||||
}
|
||||
|
||||
// 任务创建成功
|
||||
toastService.success("交叉评查任务创建成功!");
|
||||
messageService.success(
|
||||
`任务创建完成!\n任务名称:${taskInfo.name}\n上传文件:${successes.length} 个\n评查人员:${userIds.length} 人`,
|
||||
`任务创建完成!\n任务名称:${taskInfo.name}\n上传文件:${uploadSuccesses.length} 个\n评查人员:${userIds.length} 人`,
|
||||
{
|
||||
title: '任务创建成功',
|
||||
confirmText: '确定',
|
||||
@@ -1183,4 +1191,4 @@ export default function CrossCheckingUpload() {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user