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>
|
||||
|
||||
Reference in New Issue
Block a user