fix: 1.将主页和法务助手对话设置成手机也能够正确加载的响应式布局。
2. 修改合同重新上传模板的可接受文件类型,修改对接的上传模板对应的接口。 3. 交叉评查任务列表去除任务名称的点击效果。 4. 交叉评查文件预览在点击完成评查的按钮后会返回任务列表并打开任务的文档列表。 5.修复点击完成评查按钮造成页面刷新。 6. 修复创建任务的第3步无法返回列表。
This commit is contained in:
@@ -218,9 +218,10 @@ export default function CrossCheckingIndex() {
|
||||
const dateTo = searchParams.get('dateTo') || '';
|
||||
const navigate = useNavigate();
|
||||
const fetcher = useFetcher();
|
||||
|
||||
|
||||
// 状态管理
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [hasAutoOpened, setHasAutoOpened] = useState(false); // 标记是否已自动打开模态框
|
||||
const [modalState, setModalState] = useState<{
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
@@ -280,7 +281,13 @@ export default function CrossCheckingIndex() {
|
||||
|
||||
// 处理文档查看 - 导航到评查详情页
|
||||
const handleViewFile = (fileId: string) => {
|
||||
navigate(`/cross-checking/result?id=${fileId}&tId=${currentTaskInfo?.taskId}&previousRoute=crossChecking`);
|
||||
const params = new URLSearchParams({
|
||||
id: fileId,
|
||||
tId: currentTaskInfo?.taskId?.toString() || '',
|
||||
tName: currentTaskInfo?.taskName || '',
|
||||
previousRoute: 'crossChecking'
|
||||
});
|
||||
navigate(`/cross-checking/result?${params.toString()}`);
|
||||
};
|
||||
|
||||
// 存储当前任务信息用于分页
|
||||
@@ -465,11 +472,38 @@ export default function CrossCheckingIndex() {
|
||||
|
||||
|
||||
|
||||
// 检测URL参数,自动打开模态框
|
||||
useEffect(() => {
|
||||
const openModal = searchParams.get('openModal');
|
||||
const taskId = searchParams.get('taskId');
|
||||
const taskName = searchParams.get('taskName');
|
||||
|
||||
if (openModal === 'true' && taskId && !hasAutoOpened) {
|
||||
console.log('[自动打开模态框] taskId:', taskId, 'taskName:', taskName);
|
||||
|
||||
// 标记已自动打开,防止重复触发
|
||||
setHasAutoOpened(true);
|
||||
|
||||
// 清除URL参数,避免刷新页面时再次打开
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.delete('openModal');
|
||||
newParams.delete('taskId');
|
||||
newParams.delete('taskName');
|
||||
setSearchParams(newParams, { replace: true });
|
||||
|
||||
// 延迟自动打开模态框,确保状态已更新
|
||||
setTimeout(() => {
|
||||
handleViewResult(Number(taskId), taskName || '任务详情');
|
||||
}, 100);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [searchParams, hasAutoOpened]);
|
||||
|
||||
// 监听fetcher状态变化 - 删除操作
|
||||
useEffect(() => {
|
||||
if (fetcher.data && fetcher.state === 'idle' && isDeleting) {
|
||||
setIsDeleting(false);
|
||||
|
||||
|
||||
const data = fetcher.data as { result?: boolean; message?: string };
|
||||
if (data.result) {
|
||||
toastService.success(data.message || '操作成功');
|
||||
@@ -533,16 +567,7 @@ export default function CrossCheckingIndex() {
|
||||
dataIndex: "taskName" as keyof CrossCheckingTask,
|
||||
key: "taskName",
|
||||
align: "left" as const,
|
||||
width: "16%",
|
||||
render: (value: string, record: CrossCheckingTask) => (
|
||||
<button
|
||||
className="task-name text-left w-full"
|
||||
onClick={() => navigate(`/cross-checking/${record.id}`)}
|
||||
type="button"
|
||||
>
|
||||
{value}
|
||||
</button>
|
||||
)
|
||||
width: "16%"
|
||||
},
|
||||
{
|
||||
title: "评查开始时间",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
*/
|
||||
|
||||
import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/node";
|
||||
import React, { useState, useEffect } from "react";
|
||||
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} from "~/api/evaluation_points/reviews";
|
||||
@@ -185,6 +185,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const url = new URL(request.url);
|
||||
const id = url.searchParams.get('id') || undefined;
|
||||
const taskId = url.searchParams.get('tId') || undefined;
|
||||
const taskName = url.searchParams.get('tName') || undefined;
|
||||
const previousRoute = url.searchParams.get('previousRoute') || '';
|
||||
// console.log("id-------",id);
|
||||
if (!id) {
|
||||
@@ -225,7 +226,9 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
scoring_proposals: reviewData.scoring_proposals || [],
|
||||
userInfo: userInfo,
|
||||
jwtToken: frontendJWT, // 传递JWT token
|
||||
isProposer: isProposer
|
||||
isProposer: isProposer,
|
||||
taskId: taskId, // 传递任务ID
|
||||
taskName: taskName // 传递任务名称
|
||||
});
|
||||
} else {
|
||||
console.error("返回的评查数据格式不正确",JSON.stringify(reviewData,null,2));
|
||||
@@ -294,14 +297,27 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
}
|
||||
|
||||
export default function CrossCheckingResult() {
|
||||
console.log('[组件] CrossCheckingResult 渲染');
|
||||
|
||||
const navigate = useNavigate();
|
||||
const loaderData = useLoaderData<typeof loader>();
|
||||
const { document, reviewPoints, statistics, reviewInfo, scoring_proposals, jwtToken, userInfo, isProposer } = loaderData;
|
||||
const { document, reviewPoints, statistics, reviewInfo, scoring_proposals, jwtToken, userInfo, isProposer, taskId, taskName } = loaderData;
|
||||
const [isLoading, setIsLoading] = useState(false); // 已经通过loader加载了数据,不需要再显示加载状态
|
||||
const [reviewData, setReviewData] = useState<ReviewData | null>(null);
|
||||
const [activeReviewPointResultId, setActiveReviewPointResultId] = useState<string | null>(null);
|
||||
const [targetPage, setTargetPage] = useState<number | undefined>(undefined);
|
||||
const [localScoringProposals, setLocalScoringProposals] = useState<ScoringProposal[]>(scoring_proposals || []); // 本地状态管理scoringProposals
|
||||
|
||||
// 使用ref来跟踪loading状态,避免不必要的重新渲染
|
||||
const isProcessingRef = useRef(false);
|
||||
|
||||
// 添加组件挂载/卸载日志
|
||||
useEffect(() => {
|
||||
console.log('[组件] CrossCheckingResult 挂载');
|
||||
return () => {
|
||||
console.log('[组件] CrossCheckingResult 卸载');
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 同步外部scoring_proposals到本地状态
|
||||
useEffect(() => {
|
||||
@@ -309,9 +325,9 @@ export default function CrossCheckingResult() {
|
||||
}, [scoring_proposals]);
|
||||
|
||||
// 处理意见提交成功的回调
|
||||
const handleOpinionSubmitted = (newProposal: ScoringProposal) => {
|
||||
const handleOpinionSubmitted = useCallback((newProposal: ScoringProposal) => {
|
||||
setLocalScoringProposals(prev => [...prev, newProposal]);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// loader 数据加载出错
|
||||
useEffect(()=>{
|
||||
@@ -371,7 +387,7 @@ export default function CrossCheckingResult() {
|
||||
}, [document, reviewPoints, statistics, reviewInfo]);
|
||||
|
||||
|
||||
const handleReviewPointSelect = (reviewPointId: string, page?: number) => {
|
||||
const handleReviewPointSelect = useCallback((reviewPointId: string, page?: number) => {
|
||||
// 如果点击的是相同的评查点,但有page参数,先重置targetPage以确保useEffect能够触发
|
||||
if (reviewPointId === activeReviewPointResultId && page) {
|
||||
setTargetPage(undefined);
|
||||
@@ -385,7 +401,7 @@ export default function CrossCheckingResult() {
|
||||
setActiveReviewPointResultId(reviewPointId);
|
||||
setTargetPage(page);
|
||||
}
|
||||
};
|
||||
}, [activeReviewPointResultId]);
|
||||
|
||||
|
||||
// 处理评审点状态变更
|
||||
@@ -500,28 +516,38 @@ export default function CrossCheckingResult() {
|
||||
* 2. 根据结果弹出确认模态框
|
||||
* 3. 用户确认后更新文档状态并跳转
|
||||
*/
|
||||
const handleConfirmResults = async (event?: React.MouseEvent) => {
|
||||
// 阻止默认行为,防止页面刷新
|
||||
const handleConfirmResults = useCallback(async (event?: React.MouseEvent) => {
|
||||
console.log('[完成评查] 开始处理');
|
||||
|
||||
// 首先阻止默认行为和事件冒泡,防止页面刷新
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
|
||||
if (!document || !document.id) {
|
||||
toastService.error('文档数据不完整,无法确认评查结果');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 使用ref防止重复点击,避免触发状态更新
|
||||
if (isProcessingRef.current) {
|
||||
console.log('[完成评查] 正在处理中,跳过');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
// 1. 先检查未投票
|
||||
console.log('[完成评查] 标记为处理中');
|
||||
isProcessingRef.current = true;
|
||||
|
||||
// 1. 先检查未投票(不触发loading状态更新,避免重新渲染)
|
||||
console.log('[完成评查] 开始检查未投票提案');
|
||||
const checkRes = await checkProposalVotes(document.id, jwtToken);
|
||||
console.log("checkRes", checkRes);
|
||||
|
||||
console.log("[完成评查] 检查结果:", checkRes);
|
||||
|
||||
if (checkRes.error) {
|
||||
toastService.error(checkRes.error);
|
||||
setIsLoading(false);
|
||||
isProcessingRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -551,7 +577,12 @@ export default function CrossCheckingResult() {
|
||||
modalMessage = '是否完成评查?';
|
||||
}
|
||||
|
||||
// 4. 弹出模态框
|
||||
// 4. 重置处理状态标记,准备显示模态框(不触发状态更新)
|
||||
console.log('[完成评查] 重置处理标记,准备显示模态框');
|
||||
isProcessingRef.current = false;
|
||||
|
||||
// 5. 弹出模态框
|
||||
console.log('[完成评查] 显示确认模态框');
|
||||
messageService.show({
|
||||
title: '提示',
|
||||
message: modalMessage,
|
||||
@@ -559,43 +590,58 @@ export default function CrossCheckingResult() {
|
||||
confirmText: '确认',
|
||||
cancelText: '取消',
|
||||
onConfirm: async () => {
|
||||
// 用户点击确认后才开始处理,此时才显示loading
|
||||
console.log('[完成评查] 用户点击确认,开始更新状态');
|
||||
setIsLoading(true);
|
||||
const res = await confirmReviewResults(document.id, jwtToken);
|
||||
setIsLoading(false);
|
||||
try {
|
||||
const res = await confirmReviewResults(document.id, jwtToken);
|
||||
|
||||
if (res.error) {
|
||||
toastService.error(res.error);
|
||||
return;
|
||||
if (res.error) {
|
||||
toastService.error(res.error);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
toastService.success('评查结果已确认,文档审核状态已更新');
|
||||
// 跳转到交叉评查列表页,并带上任务信息以自动打开模态框
|
||||
const params = new URLSearchParams({
|
||||
openModal: 'true',
|
||||
taskId: taskId || '',
|
||||
taskName: taskName || '任务详情'
|
||||
});
|
||||
navigate(`/cross-checking?${params.toString()}`);
|
||||
} catch (error) {
|
||||
console.error('确认评查结果失败:', error);
|
||||
toastService.error('确认评查结果失败,请重试');
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
toastService.success('评查结果已确认,文档审核状态已更新');
|
||||
// 注释掉自动跳转,让用户停留在当前页面
|
||||
navigate('/cross-checking');
|
||||
},
|
||||
onCancel: () => {
|
||||
// 用户取消时不需要做任何处理
|
||||
console.log('[完成评查] 用户取消了确认操作');
|
||||
}
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
isProcessingRef.current = false;
|
||||
toastService.error(`确认评查结果失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
}
|
||||
};
|
||||
}, [document, jwtToken, navigate]);
|
||||
|
||||
// 构建自定义面包屑项
|
||||
const getBreadcrumbItems = () => {
|
||||
// 构建自定义面包屑项 - 使用 useMemo 缓存
|
||||
const breadcrumbItems = useMemo(() => {
|
||||
const items = [
|
||||
{ title: "交叉评查详情", to: `/cross-checking/result?id=${document?.id}` }
|
||||
];
|
||||
|
||||
|
||||
// 添加前置路由
|
||||
if (loaderData.previousRoute) {
|
||||
if (loaderData.previousRoute === 'crossChecking') {
|
||||
items.unshift({ title: "交叉评查", to: "/cross-checking" });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return items;
|
||||
};
|
||||
}, [document?.id, loaderData.previousRoute]);
|
||||
|
||||
return (
|
||||
<div className="cross-checking-result-container">
|
||||
@@ -608,8 +654,8 @@ export default function CrossCheckingResult() {
|
||||
<>
|
||||
{/* 自定义面包屑 */}
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Breadcrumb
|
||||
items={getBreadcrumbItems()}
|
||||
<Breadcrumb
|
||||
items={breadcrumbItems}
|
||||
className="items-center flex !mb-0"
|
||||
/>
|
||||
|
||||
@@ -638,17 +684,29 @@ export default function CrossCheckingResult() {
|
||||
|
||||
{/* 完成评查按钮 */}
|
||||
{isProposer && (
|
||||
<button
|
||||
<button
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
// 立即阻止所有默认行为
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleConfirmResults(event);
|
||||
// 异步调用处理函数
|
||||
void handleConfirmResults(event);
|
||||
}}
|
||||
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={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"
|
||||
>
|
||||
<i className="ri-check-double-line mr-1.5"></i>
|
||||
完成评查
|
||||
{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>
|
||||
|
||||
@@ -837,10 +837,9 @@ export default function CrossCheckingUpload() {
|
||||
</div>
|
||||
|
||||
{/* 文件上传区域 */}
|
||||
<Form method="post" encType="multipart/form-data">
|
||||
<input type="hidden" name="caseType" value={caseType} />
|
||||
<input type="hidden" name="uploadType" value={uploadType} />
|
||||
|
||||
|
||||
{/* 上传框区域 */}
|
||||
<div className="upload-section">
|
||||
{/* 单案件导入 */}
|
||||
@@ -958,30 +957,29 @@ export default function CrossCheckingUpload() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 完成按钮 */}
|
||||
<div className="flex justify-between items-center mt-8">
|
||||
<Button
|
||||
type="default"
|
||||
icon="ri-arrow-left-line"
|
||||
onClick={() => {
|
||||
console.log('点击返回列表按钮');
|
||||
navigate('/cross-checking');
|
||||
}}
|
||||
{/* 完成按钮 */}
|
||||
<div className="flex justify-between items-center mt-8">
|
||||
<Button
|
||||
type="default"
|
||||
icon="ri-arrow-left-line"
|
||||
onClick={() => {
|
||||
console.log('点击返回列表按钮');
|
||||
navigate('/cross-checking');
|
||||
}}
|
||||
>
|
||||
返回列表
|
||||
</Button>
|
||||
<div className="flex space-x-4">
|
||||
<Button type="default" onClick={handlePrev}>上一步</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={!canComplete || isUploading || isCreatingTask}
|
||||
onClick={handleCreateTask}
|
||||
>
|
||||
返回列表
|
||||
{isCreatingTask ? "创建任务中..." : isUploading ? "上传中..." : "开始创建任务"}
|
||||
</Button>
|
||||
<div className="flex space-x-4">
|
||||
<Button type="default" onClick={handlePrev}>上一步</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={!canComplete || isUploading || isCreatingTask}
|
||||
onClick={handleCreateTask}
|
||||
>
|
||||
{isCreatingTask ? "创建任务中..." : isUploading ? "上传中..." : "开始创建任务"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
{/* 文件选择状态提示 */}
|
||||
{!canComplete && !isUploading && (
|
||||
|
||||
@@ -130,6 +130,14 @@ async function handleFileUpload(
|
||||
jwtToken?: string,
|
||||
attachments?: File[]
|
||||
): Promise<FileUploadResponse> {
|
||||
// console.log('【handleFileUpload】开始上传:', {
|
||||
// fileName,
|
||||
// fileSize: binaryData.byteLength,
|
||||
// documentType,
|
||||
// hasAttachments: !!(attachments && attachments.length > 0),
|
||||
// attachmentCount: attachments?.length || 0
|
||||
// });
|
||||
|
||||
const response = await uploadDocumentToServer(
|
||||
binaryData,
|
||||
fileName,
|
||||
@@ -144,11 +152,20 @@ async function handleFileUpload(
|
||||
jwtToken,
|
||||
attachments
|
||||
);
|
||||
|
||||
|
||||
// console.log('【handleFileUpload】uploadDocumentToServer返回:', {
|
||||
// hasError: !!response.error,
|
||||
// hasData: !!response.data,
|
||||
// error: response.error,
|
||||
// dataKeys: response.data ? Object.keys(response.data) : []
|
||||
// });
|
||||
|
||||
if (response.error || !response.data) {
|
||||
console.error('【handleFileUpload】上传失败:', response.error);
|
||||
throw new Error(response.error || '上传失败');
|
||||
}
|
||||
|
||||
|
||||
// console.log('【handleFileUpload】返回数据:', response.data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -1088,7 +1105,21 @@ export default function FilesUpload() {
|
||||
attachmentFiles
|
||||
);
|
||||
|
||||
if (!uploadResp.result) throw new Error('主文件上传失败');
|
||||
// console.log('【合同上传】服务器响应数据:', uploadResp);
|
||||
// console.log('【合同上传】响应详情:', {
|
||||
// success: uploadResp.success,
|
||||
// hasResult: !!uploadResp.result,
|
||||
// error: uploadResp.error,
|
||||
// fullResponse: JSON.stringify(uploadResp)
|
||||
// });
|
||||
|
||||
if (!uploadResp.success) {
|
||||
throw new Error(uploadResp.error || '上传失败,服务器返回success=false');
|
||||
}
|
||||
|
||||
if (!uploadResp.result) {
|
||||
throw new Error('主文件上传失败:服务器未返回文档信息');
|
||||
}
|
||||
const documentId = uploadResp.result.id;
|
||||
|
||||
// 可选:模板上传
|
||||
|
||||
@@ -702,7 +702,8 @@ export default function ReviewDetails() {
|
||||
previousRoute: loaderData.previousRoute,
|
||||
path: document?.path,
|
||||
auditStatus: document?.auditStatus,
|
||||
type: document?.type
|
||||
type: document?.type,
|
||||
comparisonId: comparison_document?.id ? Number(comparison_document.id) : undefined
|
||||
}}
|
||||
onConfirmResults={handleConfirmResults}
|
||||
jwtToken={frontendJWT}
|
||||
|
||||
Reference in New Issue
Block a user