测通完成评查,投票,意见列表,任务列表,任务关联文档列表的内容。剩余创建任务,提出意见的完善

This commit is contained in:
2025-07-23 10:22:51 +08:00
parent 47664fc0e8
commit 8800e982ab
13 changed files with 750 additions and 331 deletions
+1
View File
@@ -0,0 +1 @@
{"projectName":"trae_docreview_m5eu"}
+156 -47
View File
@@ -1,4 +1,24 @@
// import { postgrestPost } from "../postgrest-client"; import { postgrestGet, postgrestPut } from "../postgrest-client";
/**
* 从不同格式的 API 响应中提取数据
* @param responseData API 响应数据
* @returns 提取后的数据或 null
*/
function extractApiData<T>(responseData: unknown): T | null {
if (!responseData) return null;
// 格式1: { code: number, msg: string, data: T }
if (typeof responseData === 'object' && responseData !== null &&
'code' in responseData &&
'data' in responseData &&
(responseData as { data: unknown }).data) {
return (responseData as { data: T }).data;
}
// 格式2: 直接是数据对象
return responseData as T;
}
/** /**
* 提出意见的请求参数接口 * 提出意见的请求参数接口
@@ -39,21 +59,8 @@ export interface CrossCheckingOpinion {
pending_voters: string[]; pending_voters: string[];
can_vote: boolean; can_vote: boolean;
problem_message: string; problem_message: string;
// 兼容旧字段 proposer_id: number;
id?: string | number; created_at: string;
evaluation_point_id?: string | number;
document_id?: string | number;
audit_point?: string;
found_issue?: string;
audit_opinion?: string;
deduction_score?: number;
status?: string;
created_at?: string;
updated_at?: string;
is_vote?: boolean;
voter_count?: number;
proposer_name?: string;
current_user_is_proposer?: boolean;
} }
/** /**
@@ -74,6 +81,34 @@ async function safeGetJWT(jwtToken?: string): Promise<string> {
return jwtToken || ''; return jwtToken || '';
} }
/**
* 获取当前登录用户是否是发起人
* @param taskId 任务ID
* @param userId 用户ID
* @returns 是否是发起人
*/
export async function findIsProposer(taskId: string | number, userId: number | undefined): Promise<boolean> {
// 通过postgrest的get请求去cross_examination_tasks表中进行查找assignee_id是否等于userId
const response = await postgrestGet(`cross_examination_tasks`, {
select: 'assigner_id',
filter: {
id: `eq.${taskId}`
}
});
if (response.error) {
console.error('获取任务数据失败:', response.error);
return false;
}
const data = extractApiData<{assigner_id: number}[]>(response.data);
// console.log('data', data);
if (data && data.length > 0) {
return data[0].assigner_id === userId;
}
return false;
}
/** /**
* 提交交叉评查意见 * 提交交叉评查意见
* @param opinionData 意见数据 * @param opinionData 意见数据
@@ -154,7 +189,7 @@ export async function getCrossCheckingOpinions(
// 如果没传userId,默认用1 // 如果没传userId,默认用1
const realUserId = userId ?? 1; const realUserId = userId ?? 1;
// 实际后端API调用,拼接API_BASE_URL // 实际后端API调用,拼接API_BASE_URL
const response = await fetch(`${API_BASE_URL}/admin/cross_review/proposals/details`, { const response = await fetch(`${API_BASE_URL}/admin/cross_review/proposals/document`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -171,7 +206,7 @@ export async function getCrossCheckingOpinions(
throw new Error('获取意见列表失败'); throw new Error('获取意见列表失败');
} }
const data = await response.json(); const data = await response.json();
console.log('最原始的返回data', data);
// 处理新的数据结构,支持分页 // 处理新的数据结构,支持分页
const responseData = data.data || data; const responseData = data.data || data;
const pagination = data.pagination; const pagination = data.pagination;
@@ -189,13 +224,8 @@ export async function getCrossCheckingOpinions(
pending_voters?: string[]; pending_voters?: string[];
can_vote?: boolean; can_vote?: boolean;
problem_message?: string; problem_message?: string;
evaluation_point_id?: string | number; proposer_id: number;
document_id?: string | number; created_at: string;
status?: string;
created_at?: string;
updated_at?: string;
is_vote?: boolean;
current_user_is_proposer?: boolean;
} }
// 适配后端返回结构,使用新字段 // 适配后端返回结构,使用新字段
@@ -211,21 +241,8 @@ export async function getCrossCheckingOpinions(
pending_voters: item.pending_voters || [], pending_voters: item.pending_voters || [],
can_vote: item.can_vote ?? false, can_vote: item.can_vote ?? false,
problem_message: item.problem_message || '', problem_message: item.problem_message || '',
// 兼容旧字段 proposer_id: item.proposer_id,
id: item.proposal_id, created_at: item.created_at
evaluation_point_id: item.evaluation_point_id,
document_id: item.document_id || documentId,
audit_point: item.evaluation_point_name,
found_issue: item.problem_message || '',
audit_opinion: item.reason || '',
deduction_score: item.proposed_score,
status: item.status || 'pending',
created_at: item.created_at || '',
updated_at: item.updated_at || '',
is_vote: item.is_vote || false,
voter_count: (item.agree_voters?.length || 0) + (item.disagree_voters?.length || 0),
proposer_name: item.proposer,
current_user_is_proposer: item.current_user_is_proposer || false
})) : []; })) : [];
return { return {
@@ -271,7 +288,8 @@ export interface OpinionActionRequest {
*/ */
export async function performOpinionAction( export async function performOpinionAction(
actionData: OpinionActionRequest, actionData: OpinionActionRequest,
jwtToken?: string jwtToken?: string,
userInfo?: { user_id: number }
): Promise<ApiResponse<{ success: boolean; message: string }>> { ): Promise<ApiResponse<{ success: boolean; message: string }>> {
try { try {
const token = await safeGetJWT(jwtToken); const token = await safeGetJWT(jwtToken);
@@ -284,23 +302,23 @@ export async function performOpinionAction(
case 'agree': case 'agree':
message = '已赞同该意见'; message = '已赞同该意见';
endpoint = `${API_BASE_URL}/admin/cross_review/proposals/${actionData.opinionId}/votes`; endpoint = `${API_BASE_URL}/admin/cross_review/proposals/${actionData.opinionId}/votes`;
requestBody = { vote_type: 'agree' }; requestBody = { vote_type: 'agree', voter_id: userInfo?.user_id };
break; break;
case 'disagree': case 'disagree':
message = '已反对该意见'; message = '已反对该意见';
endpoint = `${API_BASE_URL}/admin/cross_review/proposals/${actionData.opinionId}/votes`; endpoint = `${API_BASE_URL}/admin/cross_review/proposals/${actionData.opinionId}/votes`;
requestBody = { vote_type: 'disagree' }; requestBody = { vote_type: 'disagree', voter_id: userInfo?.user_id };
break; break;
case 'withdraw_vote': case 'withdraw_vote':
message = '已撤销投票'; message = '已撤销投票';
// 撤销投票的接口,根据实际API调整 // 撤销投票的接口,根据实际API调整
endpoint = `${API_BASE_URL}/admin/cross_review/proposals/${actionData.opinionId}/votes/withdraw`; endpoint = `${API_BASE_URL}/admin/cross_review/proposals/${actionData.opinionId}/votes`;
requestBody = {}; requestBody = { vote_type: 'cancel', voter_id: userInfo?.user_id };
break; break;
case 'withdraw_opinion': case 'withdraw_opinion':
message = '已撤销意见'; message = '已撤销意见';
// 撤销意见的接口,根据实际API调整 // 撤销意见的接口,根据实际API调整
endpoint = `${API_BASE_URL}/admin/cross_review/proposals/${actionData.opinionId}/withdraw`; endpoint = `${API_BASE_URL}/admin/cross_review/proposals/${actionData.opinionId}`;
requestBody = {}; requestBody = {};
break; break;
default: default:
@@ -308,7 +326,7 @@ export async function performOpinionAction(
} }
const response = await fetch(endpoint, { const response = await fetch(endpoint, {
method: 'POST', method: actionData.action === 'withdraw_opinion' ? 'DELETE' : 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
@@ -318,6 +336,8 @@ export async function performOpinionAction(
const data = await response.json(); const data = await response.json();
console.log('返回的意见列表数据',data);
if (!response.ok) { if (!response.ok) {
throw new Error(data.message || data.error || '操作失败'); throw new Error(data.message || data.error || '操作失败');
} }
@@ -336,3 +356,92 @@ export async function performOpinionAction(
}; };
} }
} }
/**
* 完成评查
* @param documentId 文档ID
* @returns 完成评查结果
*/
export async function confirmReviewResults(
documentId: string | number
): Promise<{data?: unknown, error?: string, status?: number}> {
try {
// 通过postgrest的post请求去documents表中进行查找id等于documentId的数据,更新documents表的audit_status为1
const response = await postgrestPut(`documents`, {
audit_status: 1
}, {
id: documentId
});
if(response.error) {
return {
error: response.error,
status: response.status
};
}
const extractedData = extractApiData<unknown>(response.data);
if(!extractedData) {
return {
error: '更新文档状态失败',
status: 500
};
}
return {
data: extractedData
};
} catch (error) {
console.error('完成评查失败:', error);
return {
error: error instanceof Error ? error.message : '完成评查失败',
status: 500
};
}
}
// 点击完成评查按钮后,调用接口,检查文档下提案是否存在未投票用户
export async function checkProposalVotes(
documentId: string | number,
jwtToken?: string
): Promise<{data?: unknown, error?: string, status?: number}> {
try {
// 获取JWT token
const token = await safeGetJWT(jwtToken);
const requestData = {
document_id: documentId
};
const response = await fetch(`${API_BASE_URL}/admin/cross_review/proposals/document/check_pending_votes`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(requestData)
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || '检查失败');
}
console.log("检查投票数据",data);
return {
data: {
success: true,
message: '检查成功',
data: data
}
};
} catch (error) {
console.error('检查失败:', error);
return {
error: error instanceof Error ? error.message : '检查失败',
status: 500
};
}
}
+11 -4
View File
@@ -104,7 +104,8 @@ export async function uploadCrossCheckingDocument(
remark: string = '', remark: string = '',
isTestDocument: boolean = false, isTestDocument: boolean = false,
documentId: number | null = null, documentId: number | null = null,
isReupload: boolean = false isReupload: boolean = false,
token: string | null = null
): Promise<{data: CrossCheckingFileUploadResponse; error?: never} | {data?: never; error: string; status?: number}> { ): Promise<{data: CrossCheckingFileUploadResponse; error?: never} | {data?: never; error: string; status?: number}> {
try { try {
console.log('【交叉评查上传】开始上传文档:', { fileName, fileSize: binaryData.byteLength, typeId }); console.log('【交叉评查上传】开始上传文档:', { fileName, fileSize: binaryData.byteLength, typeId });
@@ -140,11 +141,17 @@ export async function uploadCrossCheckingDocument(
// 发送请求 // 发送请求
try { try {
console.log('【交叉评查上传】开始fetch请求...'); console.log('【交叉评查上传】开始fetch请求...');
const headers: HeadersInit = {
'X-File-Name': encodeURIComponent(fileName),
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch(uploadUrl, { const response = await fetch(uploadUrl, {
method: 'POST', method: 'POST',
headers: { headers,
'X-File-Name': encodeURIComponent(fileName)
},
body: formData body: formData
}); });
+41 -15
View File
@@ -1,4 +1,5 @@
import { get } from '../axios-client'; import { get } from '../axios-client';
import { API_BASE_URL } from '../../config/api-config';
// 用户信息接口 // 用户信息接口
export interface UserInfo { export interface UserInfo {
@@ -48,27 +49,52 @@ export interface ApiResponse<T> {
* @param includeUsers 是否包含用户信息 * @param includeUsers 是否包含用户信息
* @returns 组织架构树 * @returns 组织架构树
*/ */
export async function getOrganizationTree(includeUsers: boolean = true): Promise<ApiResponse<OrganizationResponse>> { export async function getOrganizationTree(includeUsers: boolean = true, jwtToken?: string): Promise<ApiResponse<OrganizationResponse>> {
try { try {
console.log('开始调用获取组织架构API'); console.log('开始调用获取组织架构API');
const response = await get<OrganizationResponse>(
`/admin/users/organizations?include_users=${includeUsers}`
);
console.log('组织架构API响应:', response); let responseData: OrganizationResponse;
if (response.error) { if (jwtToken) {
console.error('获取组织架构失败:', response.error); // 如果提供了JWT Token,则使用fetch并携带Authorization头
return { const url = `${API_BASE_URL}/admin/users/organizations?include_users=${includeUsers}`;
success: false, const response = await fetch(url, {
error: response.error headers: {
}; 'Authorization': `Bearer ${jwtToken}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorText = await response.text();
console.error('获取组织架构失败 (fetch):', errorText);
return {
success: false,
error: `HTTP error! status: ${response.status}, ${errorText}`
};
}
responseData = await response.json();
} else {
// 否则,使用原有的get方法
const response = await get<OrganizationResponse>(
`/admin/users/organizations?include_users=${includeUsers}`
);
if (response.error || !response.data) {
console.error('获取组织架构失败 (get):', response.error);
return {
success: false,
error: response.error || '获取组织架构数据失败'
};
}
responseData = response.data;
} }
console.log('组织架构API响应:', responseData);
return { return {
success: true, success: true,
data: response.data data: responseData
}; };
} catch (error) { } catch (error) {
console.error('获取组织架构失败:', error); console.error('获取组织架构失败:', error);
@@ -211,4 +237,4 @@ export async function getFlatOrganizations(includeUsers: boolean = true): Promis
error: error instanceof Error ? error.message : '获取扁平化组织列表失败' error: error instanceof Error ? error.message : '获取扁平化组织列表失败'
}; };
} }
} }
@@ -65,6 +65,31 @@ export function DocumentListModal({
onViewFile(fileId); onViewFile(fileId);
} }
}; };
// 审核状态选项及样式 - 与documents._index.tsx保持一致
const auditStatusMapping: Record<string, { label: string; color: string; icon: string }> = {
"-1": { label: "不通过", color: "red", icon: "ri-close-line" },
"-2": { label: "警告", color: "yellow", icon: "ri-alert-line" },
"0": { label: "待审核", color: "blue", icon: "ri-time-line" },
"1": { label: "通过", color: "green", icon: "ri-check-line" },
"2": { label: "审核中", color: "purple", icon: "ri-search-line" },
};
// 渲染审核状态
const renderAuditStatus = (file: TaskDocument) => {
// 处理audit_status为null或undefined的情况,默认为0(待审核)
const auditStatus = file.audit_status != null ? file.audit_status : 0;
const statusKey = auditStatus.toString();
const statusInfo = auditStatusMapping[statusKey] || auditStatusMapping["0"];
return (
<div className={`inline-flex items-center px-2 py-1 rounded-full text-xs bg-${statusInfo.color}-100 text-${statusInfo.color}-800`}>
<i className={`${statusInfo.icon} mr-1`}></i>
<span>{statusInfo.label}</span>
</div>
);
};
// 渲染问题摘要 // 渲染问题摘要
const renderIssues = (file: TaskDocument) => { const renderIssues = (file: TaskDocument) => {
@@ -143,7 +168,7 @@ export function DocumentListModal({
{ {
title: "文件类型", title: "文件类型",
key: "fileType", key: "fileType",
width: "10%", width: "8%",
render: (_: unknown, file: TaskDocument) => ( render: (_: unknown, file: TaskDocument) => (
<FileTypeTag <FileTypeTag
type="other" type="other"
@@ -158,7 +183,7 @@ export function DocumentListModal({
{ {
title: "上传时间", title: "上传时间",
key: "uploadTime", key: "uploadTime",
width: "12%", width: "8%",
render: (_: unknown, file: TaskDocument) => { render: (_: unknown, file: TaskDocument) => {
const uploadTime = formatDate(file.upload_time).split(' '); const uploadTime = formatDate(file.upload_time).split(' ');
const date = uploadTime[0]; const date = uploadTime[0];
@@ -175,7 +200,7 @@ export function DocumentListModal({
{ {
title: "评查统计", title: "评查统计",
key: "reviewStatus", key: "reviewStatus",
width: "12%", width: "10%",
render: (_: unknown, file: TaskDocument) => render: (_: unknown, file: TaskDocument) =>
// 要文件切分处理完之后,再显示评查统计 // 要文件切分处理完之后,再显示评查统计
file.status === 'Processed' ? ( file.status === 'Processed' ? (
@@ -225,7 +250,7 @@ export function DocumentListModal({
key: "score", key: "score",
width: "8%", width: "8%",
render: (_: unknown, file: TaskDocument) => ( render: (_: unknown, file: TaskDocument) => (
<div className="text-center"> <div className="text-left">
{file.final_score ? ( {file.final_score ? (
<span className={`font-medium ${ <span className={`font-medium ${
file.final_score >= 90 ? 'text-green-600' : file.final_score >= 90 ? 'text-green-600' :
@@ -240,6 +265,12 @@ export function DocumentListModal({
</div> </div>
) )
}, },
{
title: '审核状态',
key: 'auditStatus',
width: '8%',
render: (_: unknown, file: TaskDocument) => renderAuditStatus(file)
},
{ {
title: "问题摘要", title: "问题摘要",
key: "issues", key: "issues",
@@ -322,4 +353,4 @@ export function DocumentListModal({
</div> </div>
</Modal> </Modal>
); );
} }
+133 -103
View File
@@ -30,7 +30,7 @@ import {
type CrossCheckingOpinion, type CrossCheckingOpinion,
type OpinionActionType type OpinionActionType
} from '../../api/cross-checking/cross-file-result'; } from '../../api/cross-checking/cross-file-result';
import { useFetcher } from '@remix-run/react'; import { useFetcher, useNavigate } from '@remix-run/react';
// import '../../styles/components/TooltipStyles.css'; // import '../../styles/components/TooltipStyles.css';
/** /**
@@ -159,6 +159,11 @@ interface ScoringProposal {
document_id: string | number; document_id: string | number;
} }
interface UserInfo {
id: number;
[key: string]: unknown;
}
interface ReviewPointsListProps { interface ReviewPointsListProps {
reviewPoints: ReviewPoint[]; reviewPoints: ReviewPoint[];
statistics: Statistics; statistics: Statistics;
@@ -167,6 +172,7 @@ interface ReviewPointsListProps {
onStatusChange?: (id: string, editAuditStatusId: string | number, status: string, message: string) => void; onStatusChange?: (id: string, editAuditStatusId: string | number, status: string, message: string) => void;
scoringProposals?: ScoringProposal[]; scoringProposals?: ScoringProposal[];
jwtToken?: string; // 添加JWT token参数 jwtToken?: string; // 添加JWT token参数
userInfo?: UserInfo; // 添加用户信息参数
} }
/** /**
@@ -424,7 +430,8 @@ export function ReviewPointsList({
activeReviewPointResultId, activeReviewPointResultId,
onReviewPointSelect, onReviewPointSelect,
scoringProposals = [], scoringProposals = [],
jwtToken jwtToken,
userInfo
}: ReviewPointsListProps) { }: ReviewPointsListProps) {
// 状态管理 // 状态管理
const [searchText, setSearchText] = useState(''); // 搜索文本 const [searchText, setSearchText] = useState(''); // 搜索文本
@@ -436,7 +443,7 @@ export function ReviewPointsList({
// 将来可以用于显示相关的评分提案信息 // 将来可以用于显示相关的评分提案信息
useEffect(() => { useEffect(() => {
if (scoringProposals && scoringProposals.length > 0) { if (scoringProposals && scoringProposals.length > 0) {
console.log('收到评分提案数据:', scoringProposals.length, '个提案'); // console.log('收到评分提案数据:', scoringProposals.length, '个提案');
// 获取提案的evaluation_result_id // 获取提案的evaluation_result_id
const evaluationResultIds = scoringProposals.map(proposal => Number(proposal.evaluation_result_id)); const evaluationResultIds = scoringProposals.map(proposal => Number(proposal.evaluation_result_id));
setEvaluationResultIds(evaluationResultIds); setEvaluationResultIds(evaluationResultIds);
@@ -471,27 +478,31 @@ export function ReviewPointsList({
// 监听fetcher状态变化 - 获取意见列表数据 // 监听fetcher状态变化 - 获取意见列表数据
useEffect(() => { useEffect(() => {
if (fetcher.data && fetcher.state === 'idle' && opinionListLoading) { if (fetcher.data && fetcher.state === 'idle' && opinionListLoading) {
const data = fetcher.data as { const data = fetcher.data as {
success?: boolean; success?: boolean;
data?: { data?: {
opinions: CrossCheckingOpinion[]; opinions: CrossCheckingOpinion[];
total: number; total: number;
}; pagination?: {
error?: string; page: number;
page_size: number;
total: number;
total_pages: number;
};
};
error?: string;
}; };
if (data.success && data.data) { if (data.success && data.data) {
console.log('意见列表数据', data.data); console.log('data.data', data.data);
setOpinionListData(data.data.opinions || []); setOpinionListData(data.data.opinions || []);
setOpinionListTotal(data.data.total || 0); setOpinionListTotal(data.data.total || 0);
// 使用当前状态值而不是依赖项中的值 if (data.data.pagination) {
setOpinionListCurrentPage(prev => prev); setOpinionListCurrentPage(data.data.pagination.page);
setOpinionListPageSize(prev => prev); setOpinionListPageSize(data.data.pagination.page_size);
}
} else { } else {
console.error('加载意见列表失败:', data.error);
toastService.error(data.error || '加载意见列表失败'); toastService.error(data.error || '加载意见列表失败');
} }
setOpinionListLoading(false); setOpinionListLoading(false);
} }
}, [fetcher.data, fetcher.state, opinionListLoading]); }, [fetcher.data, fetcher.state, opinionListLoading]);
@@ -568,12 +579,11 @@ export function ReviewPointsList({
const loadOpinionListData = async (page: number = 1, pageSize: number = 10, documentId?: string | number) => { const loadOpinionListData = async (page: number = 1, pageSize: number = 10, documentId?: string | number) => {
// 使用传入的documentId或者从selectedReviewPoint获取 // 使用传入的documentId或者从selectedReviewPoint获取
const targetDocumentId = documentId || selectedReviewPoint?.documentId; const targetDocumentId = documentId || selectedReviewPoint?.documentId;
console.log('加载意见列表数据', targetDocumentId);
if (!targetDocumentId) return; if (!targetDocumentId) return;
setOpinionListLoading(true); setOpinionListLoading(true);
try { try {
console.log('加载意见列表数据', targetDocumentId, page, pageSize);
// 使用 fetcher 调用路由的 action // 使用 fetcher 调用路由的 action
const formData = new FormData(); const formData = new FormData();
@@ -595,8 +605,8 @@ export function ReviewPointsList({
* 打开意见列表模态框 * 打开意见列表模态框
*/ */
const handleOpenOpinionListModal = (reviewPoint: ReviewPoint) => { const handleOpenOpinionListModal = (reviewPoint: ReviewPoint) => {
console.log('查看reviewPoints', reviewPoints); console.log('查看reviewPoint', reviewPoint);
if (scoringProposals.length+1 === 0) { if (scoringProposals.length === 0) {
toastService.warning('当前文件尚未有人提出过意见'); toastService.warning('当前文件尚未有人提出过意见');
return; return;
} }
@@ -626,7 +636,7 @@ export function ReviewPointsList({
setPerformingAction(actionKey); setPerformingAction(actionKey);
try { try {
const response = await performOpinionAction({ opinionId, action }, jwtToken); const response = await performOpinionAction({ opinionId, action }, jwtToken, userInfo as { user_id: number } | undefined);
if (response.error) { if (response.error) {
toastService.error(response.error); toastService.error(response.error);
@@ -634,12 +644,14 @@ export function ReviewPointsList({
} }
toastService.success(response.data?.message || '操作成功'); toastService.success(response.data?.message || '操作成功');
// console.log('即将重新加载数据');
// 重新加载数据 // 重新加载数据
await loadOpinionListData(opinionListCurrentPage, opinionListPageSize); await loadOpinionListData(opinionListCurrentPage, opinionListPageSize);
} catch (error) { } catch (error) {
console.error('操作失败:', error); console.error('操作失败:', error);
toastService.error('操作失败,请稍后重试'); toastService.error(error instanceof Error ? error.message : '操作失败,请稍后重试');
} finally { } finally {
setPerformingAction(null); setPerformingAction(null);
} }
@@ -649,6 +661,7 @@ export function ReviewPointsList({
* 处理意见列表分页变化 * 处理意见列表分页变化
*/ */
const handleOpinionListPageChange = (page: number) => { const handleOpinionListPageChange = (page: number) => {
setOpinionListCurrentPage(page);
loadOpinionListData(page, opinionListPageSize); loadOpinionListData(page, opinionListPageSize);
}; };
@@ -656,6 +669,7 @@ export function ReviewPointsList({
* 处理意见列表每页大小变化 * 处理意见列表每页大小变化
*/ */
const handleOpinionListPageSizeChange = (size: number) => { const handleOpinionListPageSizeChange = (size: number) => {
setOpinionListPageSize(size);
loadOpinionListData(1, size); loadOpinionListData(1, size);
}; };
@@ -2550,7 +2564,7 @@ export function ReviewPointsList({
{ {
title: "问题描述", title: "问题描述",
key: "problem_message", key: "problem_message",
width: "20%", width: "18%",
render: (_: unknown, record: CrossCheckingOpinion) => ( render: (_: unknown, record: CrossCheckingOpinion) => (
<div className="text-sm text-left">{record.problem_message}</div> <div className="text-sm text-left">{record.problem_message}</div>
) )
@@ -2566,7 +2580,7 @@ export function ReviewPointsList({
{ {
title: "调整分数", title: "调整分数",
key: "proposed_score", key: "proposed_score",
width: "8%", width: "5%",
align: "center" as const, align: "center" as const,
render: (_: unknown, record: CrossCheckingOpinion) => ( render: (_: unknown, record: CrossCheckingOpinion) => (
<span className={`text-sm font-medium ${record.proposed_score >= 0 ? 'text-green-600' : 'text-red-600'}`}> <span className={`text-sm font-medium ${record.proposed_score >= 0 ? 'text-green-600' : 'text-red-600'}`}>
@@ -2576,8 +2590,8 @@ export function ReviewPointsList({
}, },
{ {
title: "投票人", title: "投票人",
key: "voter_count", key: "votes",
width: "8%", width: "25%",
align: "center" as const, align: "center" as const,
render: (_: unknown, record: CrossCheckingOpinion) => { render: (_: unknown, record: CrossCheckingOpinion) => {
// 投票类型配置 // 投票类型配置
@@ -2604,7 +2618,6 @@ export function ReviewPointsList({
border: "border border-gray-200" border: "border border-gray-200"
} }
]; ];
return ( return (
<div className="flex flex-col gap-1.5 py-1 min-w-[120px]"> <div className="flex flex-col gap-1.5 py-1 min-w-[120px]">
{voterGroups.map((group) => ( {voterGroups.map((group) => (
@@ -2633,7 +2646,7 @@ export function ReviewPointsList({
{ {
title: "意见发起人", title: "意见发起人",
key: "proposer", key: "proposer",
width: "10%", width: "4%",
render: (_: unknown, record: CrossCheckingOpinion) => ( render: (_: unknown, record: CrossCheckingOpinion) => (
<div className="flex items-center justify-center"> <div className="flex items-center justify-center">
<span <span
@@ -2644,6 +2657,14 @@ export function ReviewPointsList({
</div> </div>
) )
}, },
{
title: "发起时间",
key: "created_at",
width: "18%",
render: (_: unknown, record: CrossCheckingOpinion) => (
<div className="text-sm text-left">{record.created_at}</div>
)
},
{ {
title: "操作", title: "操作",
key: "operation", key: "operation",
@@ -2652,19 +2673,19 @@ export function ReviewPointsList({
render: (_: unknown, record: CrossCheckingOpinion) => { render: (_: unknown, record: CrossCheckingOpinion) => {
const isPerforming = (action: string) => performingAction === `${record.proposal_id}-${action}`; const isPerforming = (action: string) => performingAction === `${record.proposal_id}-${action}`;
return ( return (
<OpinionActions record={record} isPerforming={isPerforming} handleOpinionAction={handleOpinionAction} /> <OpinionActions record={record} isPerforming={isPerforming} handleOpinionAction={handleOpinionAction} userInfo={userInfo as { user_id: number } | undefined} />
); );
} }
} }
]} ]}
dataSource={opinionListData} dataSource={opinionListData}
rowKey="id" rowKey="proposal_id"
emptyText="暂无意见数据" emptyText="暂无意见数据"
className="opinion-list-table" className="opinion-list-table"
/> />
{/* 分页组件 */} {/* 分页组件 */}
{opinionListTotal > opinionListPageSize && ( {opinionListTotal > 0 && (
<Pagination <Pagination
currentPage={opinionListCurrentPage} currentPage={opinionListCurrentPage}
total={opinionListTotal} total={opinionListTotal}
@@ -2673,7 +2694,7 @@ export function ReviewPointsList({
onPageSizeChange={handleOpinionListPageSizeChange} onPageSizeChange={handleOpinionListPageSizeChange}
showTotal={true} showTotal={true}
showPageSizeChanger={true} showPageSizeChanger={true}
pageSizeOptions={[10, 20, 30, 50]} pageSizeOptions={[5,10, 20, 30, 50]}
/> />
)} )}
</> </>
@@ -2686,27 +2707,24 @@ export function ReviewPointsList({
} }
// 操作按钮区美化+弹窗确认组件 // 操作按钮区美化+弹窗确认组件
function OpinionActions({ record, isPerforming, handleOpinionAction }: { function OpinionActions({ record, isPerforming, handleOpinionAction, userInfo }: {
record: CrossCheckingOpinion; record: CrossCheckingOpinion;
isPerforming: (action: string) => boolean; isPerforming: (action: string) => boolean;
handleOpinionAction: (id: string | number, action: OpinionActionType) => void; handleOpinionAction: (id: string | number, action: OpinionActionType) => void;
userInfo?: { user_id: number };
}) { }) {
const canVote = record.can_vote !== false; const [showModal, setShowModal] = useState<null | OpinionActionType>(null);
const [showWithdrawModal, setShowWithdrawModal] = useState(false);
const [withdrawType, setWithdrawType] = useState<'withdraw_vote' | 'withdraw_opinion' | null>(null);
const [countdown, setCountdown] = useState(3); const [countdown, setCountdown] = useState(3);
const [counting, setCounting] = useState(false); const [counting, setCounting] = useState(false);
const handleWithdraw = (type: 'withdraw_vote' | 'withdraw_opinion') => {
setWithdrawType(type);
setShowWithdrawModal(true);
setCountdown(3);
setCounting(true);
};
useEffect(() => { useEffect(() => {
let timer: NodeJS.Timeout; let timer: NodeJS.Timeout;
if (showWithdrawModal && counting && countdown > 0) { if (
showModal &&
(showModal === 'withdraw_opinion' || showModal === 'withdraw_vote') &&
counting &&
countdown > 0
) {
timer = setTimeout(() => { timer = setTimeout(() => {
setCountdown((c) => c - 1); setCountdown((c) => c - 1);
}, 1000); }, 1000);
@@ -2714,89 +2732,108 @@ function OpinionActions({ record, isPerforming, handleOpinionAction }: {
setCounting(false); setCounting(false);
} }
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [showWithdrawModal, counting, countdown]); }, [showModal, counting, countdown]);
const handleWithdrawConfirm = () => { const handleConfirm = () => {
if (withdrawType && countdown === 0) { if (showModal === 'withdraw_opinion' || showModal === 'withdraw_vote') {
handleOpinionAction(record.proposal_id, withdrawType); if (countdown === 0) {
setShowWithdrawModal(false); handleOpinionAction(record.proposal_id, showModal);
setWithdrawType(null); setShowModal(null);
setCountdown(3);
setCounting(false);
}
} else {
// 赞同/反对等操作直接执行
handleOpinionAction(record.proposal_id, showModal!);
setShowModal(null);
setCountdown(3); setCountdown(3);
setCounting(false); setCounting(false);
} }
}; };
const handleWithdrawCancel = () => { const handleCancel = () => {
setShowWithdrawModal(false); setShowModal(null);
setWithdrawType(null);
setCountdown(3); setCountdown(3);
setCounting(false); setCounting(false);
}; };
// 判断是否是发起人
const isProposer = userInfo && record.proposer_id === userInfo.user_id;
return ( return (
<div className="flex gap-3"> <div className="flex gap-3">
<Button {/* 仅当can_vote为true时显示赞同/反对按钮 */}
type="default" {record.can_vote && (
className="bg-green-700 hover:bg-green-800 text-white min-w-[80px] h-14 text-base font-medium rounded-lg flex items-center justify-center whitespace-nowrap shadow-md hover:shadow-lg transition-all duration-200 px-4 py-3" <>
onClick={() => handleOpinionAction(record.proposal_id, 'agree')} <Button
disabled={isPerforming('agree') || !canVote} type="default"
> className="bg-green-700 hover:bg-green-800 text-white min-w-[80px] h-14 text-base font-medium rounded-lg flex items-center justify-center whitespace-nowrap shadow-md hover:shadow-lg transition-all duration-200 px-4 py-3"
{isPerforming('agree') ? '处理中...' : '赞同'} onClick={() => { setShowModal('agree'); }}
</Button> disabled={isPerforming('agree')}
<Button >
type="default" {isPerforming('agree') ? '处理中...' : '赞同'}
className="bg-red-700 hover:bg-red-800 text-white min-w-[80px] h-14 text-base font-medium rounded-lg flex items-center justify-center whitespace-nowrap shadow-md hover:shadow-lg transition-all duration-200 px-4 py-3" </Button>
onClick={() => handleOpinionAction(record.proposal_id, 'disagree')} <Button
disabled={isPerforming('disagree') || !canVote} type="default"
> className="bg-red-700 hover:bg-red-800 text-white min-w-[80px] h-14 text-base font-medium rounded-lg flex items-center justify-center whitespace-nowrap shadow-md hover:shadow-lg transition-all duration-200 px-4 py-3"
{isPerforming('disagree') ? '处理中...' : '反对'} onClick={() => { setShowModal('disagree'); }}
</Button> disabled={isPerforming('disagree')}
{(!canVote || record.is_vote) && ( >
{isPerforming('disagree') ? '处理中...' : '反对'}
</Button>
</>
)}
{/* 仅当can_vote为false时显示撤销投票按钮 */}
{!record.can_vote && (
<Button <Button
type="default" type="default"
className="bg-yellow-600 hover:bg-yellow-700 text-white min-w-[80px] h-14 text-base font-medium rounded-lg flex items-center justify-center whitespace-nowrap shadow-md hover:shadow-lg transition-all duration-200 px-4 py-3" className="bg-yellow-600 hover:bg-yellow-700 text-white min-w-[80px] h-14 text-base font-medium rounded-lg flex items-center justify-center whitespace-nowrap shadow-md hover:shadow-lg transition-all duration-200 px-4 py-3"
onClick={() => handleWithdraw('withdraw_vote')} onClick={() => { setShowModal('withdraw_vote'); setCounting(true); }}
disabled={isPerforming('withdraw_vote')} disabled={isPerforming('withdraw_vote')}
> >
{isPerforming('withdraw_vote') ? '处理中...' : '撤销投票'} {isPerforming('withdraw_vote') ? '处理中...' : '撤销投票'}
</Button> </Button>
)} )}
{record.current_user_is_proposer && ( {/* 仅当是发起人才显示撤销意见按钮 */}
{isProposer && (
<Button <Button
type="default" type="default"
className="bg-yellow-600 hover:bg-yellow-700 text-white min-w-[80px] h-14 text-base font-medium rounded-lg flex items-center justify-center whitespace-nowrap shadow-md hover:shadow-lg transition-all duration-200 px-4 py-3" className="bg-yellow-600 hover:bg-red-700 text-white min-w-[80px] h-14 text-base font-medium rounded-lg flex items-center justify-center whitespace-nowrap shadow-md hover:shadow-lg transition-all duration-200 px-4 py-3"
onClick={() => handleWithdraw('withdraw_opinion')} onClick={() => { setShowModal('withdraw_opinion'); setCounting(true); }}
disabled={isPerforming('withdraw_opinion')} disabled={isPerforming('withdraw_opinion')}
> >
{isPerforming('withdraw_opinion') ? '处理中...' : '撤销意见'} {isPerforming('withdraw_opinion') ? '处理中...' : '撤销意见'}
</Button> </Button>
)} )}
{showWithdrawModal && ( {/* 确认操作模态框 */}
{showModal && (
<Modal <Modal
isOpen={showWithdrawModal} isOpen={!!showModal}
onClose={handleWithdrawCancel} onClose={handleCancel}
title="确认撤销" title="确认操作"
size="small" size="small"
className=""
footer={ footer={
<div className="flex justify-end gap-3"> <div className="flex justify-end gap-3">
<Button <Button
type="default" type="default"
className="min-w-[80px] h-14 text-base font-medium rounded-lg flex items-center justify-center whitespace-nowrap bg-gray-500 hover:bg-gray-600 text-white shadow-md hover:shadow-lg transition-all duration-200 px-4 py-3" className="min-w-[80px] h-14 text-base font-medium rounded-lg flex items-center justify-center whitespace-nowrap bg-gray-500 hover:bg-gray-600 text-white shadow-md hover:shadow-lg transition-all duration-200 px-4 py-3"
onClick={handleWithdrawCancel} onClick={handleCancel}
> >
</Button> </Button>
<Button <Button
type="default" type="default"
className={`bg-red-700 hover:bg-red-800 text-white min-w-[80px] h-14 text-base font-medium rounded-lg flex items-center justify-center whitespace-nowrap shadow-md hover:shadow-lg transition-all duration-200 px-4 py-3 ${countdown > 0 ? 'opacity-60 cursor-not-allowed' : ''}`} className={`bg-green-700 hover:bg-green-800 text-white min-w-[80px] h-14 text-base font-medium rounded-lg flex items-center justify-center whitespace-nowrap shadow-md hover:shadow-lg transition-all duration-200 px-4 py-3 ${(showModal === 'withdraw_opinion' || showModal === 'withdraw_vote') && countdown > 0 ? 'opacity-60 cursor-not-allowed' : ''}`}
onClick={handleWithdrawConfirm} onClick={handleConfirm}
disabled={countdown > 0} disabled={(showModal === 'withdraw_opinion' || showModal === 'withdraw_vote') && countdown > 0}
> >
{countdown > 0 ? `确认撤销(${countdown})` : '确认撤销'} {(showModal === 'withdraw_opinion' || showModal === 'withdraw_vote') && countdown > 0 ? `确认(${countdown})` : '确认'}
</Button> </Button>
</div> </div>
} }
> >
<div className="flex flex-col items-center justify-center text-base text-gray-700 py-4 text-center"> <div className="flex flex-col items-center justify-center text-base text-gray-700 py-4 text-center">
<div className="mb-2"></div> <div className="mb-2"></div>
<div className="text-sm text-gray-500"><span className="font-bold text-primary">{record.evaluation_point_name || record.proposal_id}</span></div> <div className="text-sm text-gray-500"><span className="font-bold text-primary">{record.evaluation_point_name || record.proposal_id}</span></div>
</div> </div>
</Modal> </Modal>
@@ -2820,6 +2857,8 @@ const openResultModal = (recordId: string) => {
// 交叉评查记录操作按钮组件 // 交叉评查记录操作按钮组件
export function ActionButtons({ record }: { record: CrossCheckingRecord }) { export function ActionButtons({ record }: { record: CrossCheckingRecord }) {
const navigate = useNavigate();
// 根据记录状态确定按钮类型 // 根据记录状态确定按钮类型
const getButtonConfig = () => { const getButtonConfig = () => {
switch (record.status) { switch (record.status) {
@@ -2828,22 +2867,14 @@ export function ActionButtons({ record }: { record: CrossCheckingRecord }) {
text: '去评查', text: '去评查',
bgColor: 'bg-blue-600', bgColor: 'bg-blue-600',
hoverColor: 'hover:bg-blue-700', hoverColor: 'hover:bg-blue-700',
icon: ( icon: <span className="ri-edit-2-line text-lg mr-1"></span>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
)
}; };
case 'in_progress': case 'in_progress':
return { return {
text: '进行中', text: '进行中',
bgColor: 'bg-gray-500', bgColor: 'bg-gray-500',
hoverColor: 'hover:bg-gray-600', hoverColor: 'hover:bg-gray-600',
icon: ( icon: <span className="ri-loader-4-line text-lg mr-1 animate-spin"></span>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-1 animate-pulse" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
)
}; };
case 'completed': case 'completed':
default: default:
@@ -2851,23 +2882,22 @@ export function ActionButtons({ record }: { record: CrossCheckingRecord }) {
text: '查看结果', text: '查看结果',
bgColor: 'bg-green-600', bgColor: 'bg-green-600',
hoverColor: 'hover:bg-green-700', hoverColor: 'hover:bg-green-700',
icon: ( icon: <span className="ri-eye-line text-lg mr-1"></span>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
)
}; };
} }
}; };
const buttonConfig = getButtonConfig(); const buttonConfig = getButtonConfig();
// 处理按钮点击事件 /**
* 处理按钮点击事件
* 使用React Router的navigate方法替代window.location.href,避免页面刷新
*/
const handleAction = () => { const handleAction = () => {
switch (record.status) { switch (record.status) {
case 'pending': case 'pending':
// 跳转到评查页面 // 使用navigate跳转到评查页面,避免页面刷新
window.location.href = `/review/${record.id}`; navigate(`/review/${record.id}`);
break; break;
case 'in_progress': case 'in_progress':
// 进行中状态不执行操作 // 进行中状态不执行操作
+1 -1
View File
@@ -180,7 +180,7 @@ export function MessageModal({
</h3> </h3>
)} )}
<div id="message-modal-content" className="message-modal-message"> <div id="message-modal-content" className="message-modal-message" style={{ whiteSpace: 'pre-line' }}>
{message} {message}
</div> </div>
+4 -1
View File
@@ -1,5 +1,6 @@
// app/components/ui/Modal.tsx // app/components/ui/Modal.tsx
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';
import modalStyles from '~/styles/components/modal.css?url'; import modalStyles from '~/styles/components/modal.css?url';
// 导出样式 // 导出样式
@@ -102,7 +103,7 @@ export function Modal({
if (!isOpen) return null; if (!isOpen) return null;
return ( const modalNode = (
<div <div
className="modal-backdrop" className="modal-backdrop"
aria-hidden="true" aria-hidden="true"
@@ -156,4 +157,6 @@ export function Modal({
/> />
</div> </div>
); );
return ReactDOM.createPortal(modalNode, document.body);
} }
+1
View File
@@ -202,6 +202,7 @@ export function Toast({
aria-live="polite" aria-live="polite"
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
style={{ zIndex: 99999, position: 'relative' }}
> >
<div className="toast-content"> <div className="toast-content">
<div className="toast-icon-wrapper"> <div className="toast-icon-wrapper">
+10 -7
View File
@@ -77,6 +77,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
getCrossCheckingTasks(params, userInfo, frontendJWT), getCrossCheckingTasks(params, userInfo, frontendJWT),
getCrossCheckingStats(userInfo, frontendJWT) getCrossCheckingStats(userInfo, frontendJWT)
]); ]);
console.log('tasksResponse', tasksResponse.data?.tasks);
if (!tasksResponse.success) { if (!tasksResponse.success) {
console.error('获取任务列表失败:', tasksResponse.error); console.error('获取任务列表失败:', tasksResponse.error);
@@ -242,9 +244,9 @@ export default function CrossCheckingIndex() {
}; };
// 处理查看结果 - 打开文档列表模态框 // 处理查看结果 - 打开文档列表模态框
const handleViewResult = async (taskId: number) => { const handleViewResult = async (taskId: number, taskName: string) => {
// 存储任务信息用于分页 // 存储任务信息用于分页
setCurrentTaskInfo({ taskId }); setCurrentTaskInfo({ taskId, taskName });
// 打开模态框 // 打开模态框
setModalState(prev => ({ setModalState(prev => ({
@@ -274,12 +276,13 @@ export default function CrossCheckingIndex() {
// 处理文档查看 - 导航到评查详情页 // 处理文档查看 - 导航到评查详情页
const handleViewFile = (fileId: string) => { const handleViewFile = (fileId: string) => {
navigate(`/cross-checking/result?id=${fileId}&previousRoute=crossChecking`); navigate(`/cross-checking/result?id=${fileId}&tId=${currentTaskInfo?.taskId}&previousRoute=crossChecking`);
}; };
// 存储当前任务信息用于分页 // 存储当前任务信息用于分页
const [currentTaskInfo, setCurrentTaskInfo] = useState<{ const [currentTaskInfo, setCurrentTaskInfo] = useState<{
taskId: number; taskId: number;
taskName: string;
} | null>(null); } | null>(null);
// 加载分页数据 // 加载分页数据
@@ -348,7 +351,7 @@ export default function CrossCheckingIndex() {
type="primary" type="primary"
size="small" size="small"
className="operation-btn primary" className="operation-btn primary"
onClick={() => handleViewResult(task.id)} onClick={() => handleViewResult(task.id,task.taskName)}
> >
<i className="ri-play-line"></i> <i className="ri-play-line"></i>
@@ -360,7 +363,7 @@ export default function CrossCheckingIndex() {
type="default" type="default"
size="small" size="small"
className="operation-btn secondary" className="operation-btn secondary"
onClick={() => handleViewResult(task.id)} onClick={() => handleViewResult(task.id,task.taskName)}
> >
<i className="ri-eye-line"></i> <i className="ri-eye-line"></i>
@@ -372,7 +375,7 @@ export default function CrossCheckingIndex() {
type="default" type="default"
size="small" size="small"
className="operation-btn secondary" className="operation-btn secondary"
onClick={() => handleViewResult(task.id)} onClick={() => handleViewResult(task.id,task.taskName)}
> >
<i className="ri-file-text-line"></i> <i className="ri-file-text-line"></i>
@@ -494,7 +497,7 @@ export default function CrossCheckingIndex() {
setModalState(prev => ({ setModalState(prev => ({
...prev, ...prev,
loading: false, loading: false,
title: `任务 ${currentTaskInfo?.taskId || ''} - 文档列表`, title: `${currentTaskInfo?.taskName || ''} - 文档列表`,
files: files || [], files: files || [],
total: total || 0, total: total || 0,
currentPage: currentPage || prev.currentPage, currentPage: currentPage || prev.currentPage,
+102 -49
View File
@@ -23,11 +23,12 @@
*/ */
import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/node"; import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/node";
import { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { useNavigate, useLoaderData } from "@remix-run/react"; import { useNavigate, useLoaderData } from "@remix-run/react";
import crossCheckingStyles from "~/styles/cross-checking-result.css?url"; import crossCheckingStyles from "~/styles/cross-checking-result.css?url";
import { getReviewPoints, updateReviewResult } from "~/api/evaluation_points/reviews"; import { getReviewPoints, updateReviewResult} from "~/api/evaluation_points/reviews";
import { toastService } from "~/components/ui/Toast"; import { toastService } from "~/components/ui/Toast";
import { confirmReviewResults, checkProposalVotes, findIsProposer } from "~/api/cross-checking/cross-file-result";
// 导入交叉评查详情页面组件 // 导入交叉评查详情页面组件
import { import {
@@ -183,19 +184,26 @@ export async function loader({ request }: LoaderFunctionArgs) {
try { try {
const url = new URL(request.url); const url = new URL(request.url);
const id = url.searchParams.get('id') || undefined; const id = url.searchParams.get('id') || undefined;
const taskId = url.searchParams.get('tId') || undefined;
const previousRoute = url.searchParams.get('previousRoute') || ''; const previousRoute = url.searchParams.get('previousRoute') || '';
// console.log("id-------",id); // console.log("id-------",id);
if (!id) { if (!id) {
return Response.json({ result: false, message: '文件ID不能为空' }); return Response.json({ result: false, message: '文件ID不能为空' });
} }
if (!taskId) {
return Response.json({ result: false, message: '任务ID不能为空' });
}
// 获取用户会话信息 // 获取用户会话信息
const { getUserSession } = await import("~/api/login/auth.server"); const { getUserSession } = await import("~/api/login/auth.server");
const { frontendJWT } = await getUserSession(request); const { userInfo, frontendJWT } = await getUserSession(request);
// 获取评查点数据,传递request对象 // 获取评查点数据,传递request对象
const reviewData = await getReviewPoints(id, request); const reviewData = await getReviewPoints(id, request);
// 获取当前登录用户是否是发起人
const isProposer = await findIsProposer(taskId, userInfo?.user_id);
// console.log("documentData-------",JSON.stringify(documentData.data,null,2)); // 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('data' in reviewData ? reviewData.data : '',null,2));
// console.log("reviewData-------",JSON.stringify(reviewData,null,2)); // console.log("reviewData-------",JSON.stringify(reviewData,null,2));
@@ -215,7 +223,9 @@ export async function loader({ request }: LoaderFunctionArgs) {
statistics: reviewData.stats, statistics: reviewData.stats,
comparison_document: reviewData.comparison_document, comparison_document: reviewData.comparison_document,
scoring_proposals: reviewData.scoring_proposals || [], scoring_proposals: reviewData.scoring_proposals || [],
jwtToken: frontendJWT // 传递JWT token userInfo: userInfo,
jwtToken: frontendJWT, // 传递JWT token
isProposer: isProposer
}); });
} else { } else {
console.error("返回的评查数据格式不正确",JSON.stringify(reviewData,null,2)); console.error("返回的评查数据格式不正确",JSON.stringify(reviewData,null,2));
@@ -298,20 +308,9 @@ export async function action({ request }: ActionFunctionArgs) {
} }
if (intent === "confirmReviewResults") { if (intent === "confirmReviewResults") {
toastService.error('确认评查结果功能暂未实现'); // 检查文档下提案是否存在未投票用户,首先先打开一个模态框,提示用户是否确认完成评查,如果用户点击确认,则调用confirmReviewResults接口,如果用户点击取消,则关闭模态框
// TODO 应该在cross-file-result.ts中新增一个确认的方法 // 模态框内的数据需要根据checkProposalVotes返回回来的数据进行显示,如果存在未投票用户,则提示用户存在未投票用户,如果不存在未投票用户,则提示用户完成评查
// const documentId = formData.get("documentId") as string;
// const response = await confirmReviewResults(documentId, request);
// if (response.error) {
// return Response.json({ success: false, error: response.error }, { status: response.status || 500 });
// }
// return Response.json({ success: true, data: response.data });
} }
return Response.json({ success: false, error: "未知的操作类型" }, { status: 400 });
} catch (error) { } catch (error) {
console.error('Action处理失败:', error); console.error('Action处理失败:', error);
return Response.json({ return Response.json({
@@ -324,7 +323,7 @@ export async function action({ request }: ActionFunctionArgs) {
export default function CrossCheckingResult() { export default function CrossCheckingResult() {
const navigate = useNavigate(); const navigate = useNavigate();
const loaderData = useLoaderData<typeof loader>(); const loaderData = useLoaderData<typeof loader>();
const { document, reviewPoints, statistics, reviewInfo, scoring_proposals, jwtToken } = loaderData; const { document, reviewPoints, statistics, reviewInfo, scoring_proposals, jwtToken, userInfo, isProposer } = loaderData;
const [isLoading, setIsLoading] = useState(false); // 已经通过loader加载了数据,不需要再显示加载状态 const [isLoading, setIsLoading] = useState(false); // 已经通过loader加载了数据,不需要再显示加载状态
const [reviewData, setReviewData] = useState<ReviewData | null>(null); const [reviewData, setReviewData] = useState<ReviewData | null>(null);
const [activeReviewPointResultId, setActiveReviewPointResultId] = useState<string | null>(null); const [activeReviewPointResultId, setActiveReviewPointResultId] = useState<string | null>(null);
@@ -511,44 +510,90 @@ export default function CrossCheckingResult() {
} }
}; };
const handleConfirmResults = async () => { /**
* 处理确认评查结果
* 1. 检查未投票提案
* 2. 根据结果弹出确认模态框
* 3. 用户确认后更新文档状态并跳转
*/
const handleConfirmResults = async (event?: React.MouseEvent) => {
// 阻止默认行为,防止页面刷新
if (event) {
event.preventDefault();
event.stopPropagation();
}
if (!document || !document.id) { if (!document || !document.id) {
toastService.error('文档数据不完整,无法确认评查结果'); toastService.error('文档数据不完整,无法确认评查结果');
return; return;
} }
try { try {
// 显示加载状态
setIsLoading(true); setIsLoading(true);
// 使用 fetch 调用 action // 1. 先检查未投票
const formData = new FormData(); const checkRes = await checkProposalVotes(document.id, jwtToken);
formData.append("intent", "confirmReviewResults"); console.log("checkRes", checkRes);
formData.append("documentId", document.id.toString());
const response = await fetch(window.location.pathname, {
method: "POST",
body: formData,
});
const result = await response.json();
if (!result.success) { if (checkRes.error) {
console.error('确认评查结果失败:', result.error); toastService.error(checkRes.error);
toastService.error(`确认评查结果失败: ${result.error}`); setIsLoading(false);
return; return;
} }
// 显示成功消息 // 2. 解析返回数据,定义明确的类型
toastService.success('评查结果已确认,文档审核状态已更新'); interface CheckProposalResponse {
success: boolean;
message: string;
data: {
pending_proposals: Array<{
evaluation_point_name: string;
pending_voters_num: number;
}>;
};
}
const responseData = checkRes.data as CheckProposalResponse;
const pendingProposals = responseData?.data?.pending_proposals || [];
console.log("pendingProposals", pendingProposals);
// 3. 构建模态框消息
let modalMessage: string = '';
if (Array.isArray(pendingProposals) && pendingProposals.length > 0) {
modalMessage = pendingProposals.map((item) =>
`评查名称为:${item.evaluation_point_name} 还剩余 ${item.pending_voters_num}人未投票。`
).join('\n');
} else {
modalMessage = '是否完成评查?';
}
// 4. 弹出模态框
messageService.show({
title: '提示',
message: modalMessage,
type: 'warning',
confirmText: '确认',
cancelText: '取消',
onConfirm: async () => {
setIsLoading(true);
const res = await confirmReviewResults(document.id);
setIsLoading(false);
if (res.error) {
toastService.error(res.error);
return;
}
toastService.success('评查结果已确认,文档审核状态已更新');
// 注释掉自动跳转,让用户停留在当前页面
navigate('/cross-checking');
}
});
// 导航到交叉评查列表页
navigate('/cross-checking');
} catch (error) {
console.error('确认评查结果出错:', error);
toastService.error(`确认评查结果失败: ${error instanceof Error ? error.message : '未知错误'}`);
} finally {
setIsLoading(false); setIsLoading(false);
} catch (error) {
setIsLoading(false);
toastService.error(`确认评查结果失败: ${error instanceof Error ? error.message : '未知错误'}`);
} }
}; };
@@ -608,13 +653,20 @@ export default function CrossCheckingResult() {
</div> </div>
{/* 完成评查按钮 */} {/* 完成评查按钮 */}
<button {isProposer && (
onClick={handleConfirmResults} <button
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" type="button"
> onClick={(event) => {
<i className="ri-check-double-line mr-1.5"></i> event.preventDefault();
event.stopPropagation();
</button> 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"
>
<i className="ri-check-double-line mr-1.5"></i>
</button>
)}
</div> </div>
{/* 文件信息和操作按钮 */} {/* 文件信息和操作按钮 */}
@@ -648,6 +700,7 @@ export default function CrossCheckingResult() {
onStatusChange={handleReviewPointStatusChange} onStatusChange={handleReviewPointStatusChange}
scoringProposals={scoring_proposals as ScoringProposal[]} scoringProposals={scoring_proposals as ScoringProposal[]}
jwtToken={jwtToken} jwtToken={jwtToken}
userInfo={userInfo}
/> />
</div> </div>
</div> </div>
+226 -56
View File
@@ -1,6 +1,6 @@
import { useState, useRef, useEffect } from "react"; import { useState, useRef, useEffect } from "react";
import { type MetaFunction, type ActionFunctionArgs } from "@remix-run/node"; import { type MetaFunction, type ActionFunctionArgs, type LoaderFunctionArgs, json } from "@remix-run/node";
import { Form, useNavigation, useNavigate } from "@remix-run/react"; import { Form, useNavigate, useLoaderData } from "@remix-run/react";
import { UploadArea, type UploadAreaRef } from "~/components/ui/UploadArea"; import { UploadArea, type UploadAreaRef } from "~/components/ui/UploadArea";
import { Button } from "~/components/ui/Button"; import { Button } from "~/components/ui/Button";
import { messageService } from "~/components/ui/MessageModal"; import { messageService } from "~/components/ui/MessageModal";
@@ -124,6 +124,60 @@ const TreeNodeCheckbox: React.FC<{
</div> </div>
); );
}; };
/**
* 获取用户会话和前端JWT
*/
export const loader = async ({ request }: LoaderFunctionArgs) => {
// 获取用户会话信息
const { getUserSession } = await import("~/api/login/auth.server");
const { userInfo, frontendJWT } = await getUserSession(request);
return json({
userInfo,
frontendJWT
});
};
/**
* 创建交叉评查任务
* @param taskData 任务数据
* @param token JWT Token
* @returns 创建结果
*/
async function createCrossReviewTask(taskData: {
documentIds: number[];
userIds: number[];
assignerId: number;
taskName: string;
}, token: string) {
try {
const response = await fetch('/admin/crossreview/tasks/assign', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
document_ids: taskData.documentIds,
user_ids: taskData.userIds,
assigner_id: taskData.assignerId,
task_name: taskData.taskName
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
console.log('任务创建成功:', result);
return result;
} catch (error) {
console.error('创建任务失败:', error);
throw error;
}
}
export const action = async ({ request }: ActionFunctionArgs) => { export const action = async ({ request }: ActionFunctionArgs) => {
const formData = await request.formData(); const formData = await request.formData();
const caseType = formData.get("caseType") as string; const caseType = formData.get("caseType") as string;
@@ -137,10 +191,15 @@ export const action = async ({ request }: ActionFunctionArgs) => {
}; };
export default function CrossCheckingUpload() { export default function CrossCheckingUpload() {
// 获取loader数据
const { userInfo, frontendJWT } = useLoaderData<typeof loader>();
// 基础状态 // 基础状态
const [caseType, setCaseType] = useState<CaseType>(CaseType.ADMINISTRATIVE_PENALTY); const [caseType, setCaseType] = useState<CaseType>(CaseType.ADMINISTRATIVE_PENALTY);
// 步骤状态 // 步骤状态
const [currentStep, setCurrentStep] = useState(1); const [currentStep, setCurrentStep] = useState(1);
// 任务创建状态
const [isCreatingTask, setIsCreatingTask] = useState(false);
// 步骤1:任务信息 // 步骤1:任务信息
const [taskInfo, setTaskInfo] = useState({ const [taskInfo, setTaskInfo] = useState({
name: '', name: '',
@@ -171,8 +230,7 @@ export default function CrossCheckingUpload() {
const singleUploadRef = useRef<UploadAreaRef>(null); const singleUploadRef = useRef<UploadAreaRef>(null);
const multipleUploadRef = useRef<UploadAreaRef>(null); const multipleUploadRef = useRef<UploadAreaRef>(null);
// 获取当前typeId
const currentTypeId = CASE_TYPE_TO_TYPE_ID[caseType];
// 处理案卷类型切换 // 处理案卷类型切换
const handleCaseTypeChange = (type: CaseType) => { const handleCaseTypeChange = (type: CaseType) => {
@@ -330,71 +388,177 @@ export default function CrossCheckingUpload() {
setUploadType('none'); setUploadType('none');
}; };
// 处理完成上传 /**
const handleCompleteUpload = async () => { * 处理创建任务
*/
const handleCreateTask = async () => {
// 验证步骤1:任务信息
if (!taskInfo.name.trim()) {
toastService.error("请填写任务名称");
return;
}
if (!taskInfo.date.trim()) {
toastService.error("请选择任务日期");
return;
}
// 验证步骤2:评查小组
if (groupChecked.length === 0) {
toastService.error("请选择评查小组成员");
return;
}
// 验证步骤3:文件上传
const filesToUpload = uploadType === 'single' ? singleFiles : multipleFiles; const filesToUpload = uploadType === 'single' ? singleFiles : multipleFiles;
if (filesToUpload.length === 0) { if (filesToUpload.length === 0) {
toastService.error("请先选择要上传的文件"); toastService.error("请先选择要上传的文件");
return; return;
} }
setIsCreatingTask(true);
setIsUploading(true); setIsUploading(true);
try { try {
console.log("开始批量上传文件:", filesToUpload.length, "个,案卷类型:", caseType, "typeId:", currentTypeId); // 第一步:上传文件
console.log("开始批量上传文件:", filesToUpload.length, "个,案卷类型:", caseType);
const result = await batchUploadCrossCheckingFiles( const uploadResult = await batchUploadCrossCheckingFiles(
filesToUpload, filesToUpload.map(f => f.file),
currentTypeId, caseType,
priority, priority,
documentNumber, isTestDocument,
remark, frontendJWT
isTestDocument
); );
const { successes, failures } = result; const { successes, failures } = uploadResult;
if (failures.length === 0) { if (failures.length > 0) {
// 全部成功 toastService.error(`文件上传失败:${failures[0].error}`);
toastService.success(`成功上传 ${successes.length} 个文件`); return;
// 立即清空文件列表
clearAllFiles();
messageService.success(`文件上传完成!成功上传 ${successes.length} 个文件,现在可以进行下一步操作。`, {
title: '上传成功',
confirmText: '确定'
});
} else if (successes.length === 0) {
// 全部失败
toastService.error(`文件上传失败,共 ${failures.length} 个文件上传失败`);
messageService.error(`所有文件上传失败。失败原因:${failures[0].error}`, {
title: '上传失败',
confirmText: '确定',
});
} else {
// 部分成功
toastService.warning(`部分文件上传成功:成功 ${successes.length} 个,失败 ${failures.length}`);
messageService.warning(
`部分文件上传完成:\n成功:${successes.length} 个文件\n失败:${failures.length} 个文件\n\n失败文件:\n${failures.map(f => `${f.file.name}: ${f.error}`).join('\n')}`,
{
title: '部分上传成功',
confirmText: '确定',
}
);
} }
// 第二步:创建交叉评查任务
console.log("文件上传成功,开始创建任务");
// 提取文档ID
const documentIds = successes.map(success => success.result.result?.id).filter(id => id !== undefined) as number[];
// 提取用户ID(从选中的组织架构中获取用户)
const userIds = groupChecked.filter(id => {
// 检查是否为用户ID(通常用户ID以特定前缀开头或有特定格式)
return id.includes('user_');
}).map(id => parseInt(id.replace('user_', '')));
if (userIds.length === 0) {
toastService.error("请选择具体的评查人员");
return;
}
// 创建任务数据
const taskData = {
documentIds,
userIds,
assignerId: userInfo?.user_id || 1, // 使用当前用户ID作为分配者
taskName: taskInfo.name
};
console.log("创建任务数据:", taskData);
// 调用创建任务接口
await createCrossReviewTask(taskData, frontendJWT);
// 任务创建成功
toastService.success("交叉评查任务创建成功!");
messageService.success(
`任务创建完成!\n任务名称:${taskInfo.name}\n上传文件:${successes.length}\n评查人员:${userIds.length}`,
{
title: '任务创建成功',
confirmText: '确定',
onConfirm: () => {
// 跳转到任务列表页面
navigate('/cross-checking');
}
}
);
} catch (error) { } catch (error) {
console.error("批量上传失败:", error); console.error("创建任务失败:", error);
toastService.error("文件上传过程中发生错误"); toastService.error(`创建任务失败:${error instanceof Error ? error.message : '未知错误'}`);
messageService.error(`文件上传失败:${error instanceof Error ? error.message : '未知错误'}`, { messageService.error(`创建任务失败:${error instanceof Error ? error.message : '未知错误'}`, {
title: '上传失败', title: '创建失败',
confirmText: '确定', confirmText: '确定',
}); });
} finally { } finally {
setIsCreatingTask(false);
setIsUploading(false); setIsUploading(false);
} }
}; };
// 处理完成上传(保留原有功能用于测试)
// 处理完成上传(保留原有功能用于测试)
// const handleCompleteUpload = async () => {
// const filesToUpload = uploadType === 'single' ? singleFiles : multipleFiles;
// if (filesToUpload.length === 0) {
// toastService.error("请先选择要上传的文件");
// return;
// }
// setIsUploading(true);
// try {
// console.log("开始批量上传文件:", filesToUpload.length, "个,案卷类型:", caseType);
// const result = await batchUploadCrossCheckingFiles(
// filesToUpload.map(f => f.file),
// caseType,
// priority,
// isTestDocument,
// frontendJWT
// );
// const { successes, failures } = result;
// if (failures.length === 0) {
// // 全部成功
// toastService.success(`成功上传 ${successes.length} 个文件`);
// // 立即清空文件列表
// clearAllFiles();
// messageService.success(`文件上传完成!成功上传 ${successes.length} 个文件,现在可以进行下一步操作。`, {
// title: '上传成功',
// confirmText: '确定'
// });
// } else if (successes.length === 0) {
// // 全部失败
// toastService.error(`文件上传失败,共 ${failures.length} 个文件上传失败`);
// messageService.error(`所有文件上传失败。失败原因:${failures[0].error}`, {
// title: '上传失败',
// confirmText: '确定',
// });
// } else {
// // 部分成功
// toastService.warning(`部分文件上传成功:成功 ${successes.length} 个,失败 ${failures.length} 个`);
// messageService.warning(
// `部分文件上传完成:\n成功:${successes.length} 个文件\n失败:${failures.length} 个文件\n\n失败文件:\n${failures.map(f => `${f.file.name}: ${f.error}`).join('\n')}`,
// {
// title: '部分上传成功',
// confirmText: '确定',
// }
// );
// }
// } catch (error) {
// console.error("批量上传失败:", error);
// toastService.error("文件上传过程中发生错误");
// messageService.error(`文件上传失败:${error instanceof Error ? error.message : '未知错误'}`, {
// title: '上传失败',
// confirmText: '确定',
// });
// } finally {
// setIsUploading(false);
// }
// };
// 步骤切换 // 步骤切换
const handleNext = () => setCurrentStep((s) => Math.min(s + 1, 3)); const handleNext = () => setCurrentStep((s) => Math.min(s + 1, 3));
const handlePrev = () => setCurrentStep((s) => Math.max(s - 1, 1)); const handlePrev = () => setCurrentStep((s) => Math.max(s - 1, 1));
@@ -405,8 +569,8 @@ export default function CrossCheckingUpload() {
// 检查是否可以完成 // 检查是否可以完成
const canComplete = (singleFiles.length > 0 || multipleFiles.length > 0) && !isUploading; const canComplete = (singleFiles.length > 0 || multipleFiles.length > 0) && !isUploading;
const navigation = useNavigation(); // const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting"; // 由于 isSubmitting 未被使用,暂时移除该行代码
const navigate = useNavigate(); const navigate = useNavigate();
@@ -419,7 +583,8 @@ export default function CrossCheckingUpload() {
try { try {
console.log('开始加载组织架构数据'); console.log('开始加载组织架构数据');
const response = await getOrganizationTree(true); // 传递JWT token到API调用
const response = await getOrganizationTree(true, frontendJWT);
if (response.success && response.data) { if (response.success && response.data) {
console.log('原始API数据:', response.data); console.log('原始API数据:', response.data);
@@ -803,10 +968,10 @@ export default function CrossCheckingUpload() {
<Button type="default" onClick={handlePrev}></Button> <Button type="default" onClick={handlePrev}></Button>
<Button <Button
type="primary" type="primary"
disabled={!canComplete || isUploading} disabled={!canComplete || isUploading || isCreatingTask}
onClick={handleCompleteUpload} onClick={handleCreateTask}
> >
{isUploading || isSubmitting ? "上传中..." : "开始创建任务"} {isCreatingTask ? "创建任务中..." : isUploading ? "上传中..." : "开始创建任务"}
</Button> </Button>
</div> </div>
</div> </div>
@@ -819,16 +984,21 @@ export default function CrossCheckingUpload() {
</div> </div>
)} )}
{/* 上传进度提示 */} {/* 创建任务进度提示 */}
{isUploading && ( {(isUploading || isCreatingTask) && (
<div className="text-center mt-4"> <div className="text-center mt-4">
<div className="bg-blue-50 p-4 rounded-md border border-blue-100"> <div className="bg-blue-50 p-4 rounded-md border border-blue-100">
<div className="flex items-center justify-center text-blue-800 mb-2"> <div className="flex items-center justify-center text-blue-800 mb-2">
<i className="ri-loader-4-line ri-spin animate-spin text-xl mr-2"></i> <i className="ri-loader-4-line ri-spin animate-spin text-xl mr-2"></i>
<span className="font-medium">...</span> <span className="font-medium">
{isCreatingTask ? "正在创建任务..." : "正在上传文件..."}
</span>
</div> </div>
<p className="text-sm text-blue-700"> <p className="text-sm text-blue-700">
{uploadType === 'single' ? singleFiles.length : multipleFiles.length} {isCreatingTask
? `正在创建交叉评查任务:${taskInfo.name}`
: `正在上传 ${uploadType === 'single' ? singleFiles.length : multipleFiles.length} 个文件,请稍候`
}
</p> </p>
</div> </div>
</div> </div>
+28 -43
View File
@@ -238,7 +238,7 @@ export async function action({ request }: ActionFunctionArgs) {
return Response.json({ success: false, error: response.error }, { status: response.status || 500 }); return Response.json({ success: false, error: response.error }, { status: response.status || 500 });
} }
return Response.json({ success: true, data: response.data }); return Response.json({ success: true, data: response.data, intent: "confirmReviewResults" });
} catch (updateError) { } catch (updateError) {
console.error('调用updateReviewResult时发生异常:', updateError); console.error('调用updateReviewResult时发生异常:', updateError);
return Response.json({ return Response.json({
@@ -258,15 +258,16 @@ export async function action({ request }: ActionFunctionArgs) {
if (response.error) { if (response.error) {
console.error('confirmReviewResults返回错误:', response.error); console.error('confirmReviewResults返回错误:', response.error);
return Response.json({ success: false, error: response.error }, { status: response.status || 500 }); return Response.json({ success: false, error: response.error, intent: "confirmReviewResults" }, { status: response.status || 500 });
} }
return Response.json({ success: true, data: response.data }); return Response.json({ success: true, data: response.data, intent: "confirmReviewResults" });
} catch (confirmError) { } catch (confirmError) {
console.error('调用confirmReviewResults时发生异常:', confirmError); console.error('调用confirmReviewResults时发生异常:', confirmError);
return Response.json({ return Response.json({
success: false, success: false,
error: confirmError instanceof Error ? confirmError.message : '确认评查结果时发生未知错误' error: confirmError instanceof Error ? confirmError.message : '确认评查结果时发生未知错误',
intent: "confirmReviewResults"
}, { status: 500 }); }, { status: 500 });
} }
} }
@@ -522,6 +523,27 @@ export default function ReviewDetails() {
} }
}, [fetcher.state, fetcher.data, pendingUpdate, document, reviewData]); }, [fetcher.state, fetcher.data, pendingUpdate, document, reviewData]);
// 监听fetcher状态变化 - 处理确认评查结果
useEffect(() => {
if (fetcher.state === "idle" && fetcher.data && !pendingUpdate) {
const result = fetcher.data as { success: boolean; error?: string; intent?: string };
// 只处理confirmReviewResults的响应
if (result.intent === 'confirmReviewResults') {
setIsLoading(false);
if (result.success) {
toastService.success('评查结果已确认,文档审核状态已更新');
// 导航到文档列表页
navigate('/documents');
} else {
console.error('确认评查结果失败:', result.error);
toastService.error(`确认评查结果失败: ${result.error || '未知错误'}`);
}
}
}
}, [fetcher.state, fetcher.data, pendingUpdate, navigate]);
// 处理评审点状态变更 // 处理评审点状态变更
const handleReviewPointStatusChange = async (reviewPointResultId: string, editAuditStatusId: string | number, newStatus: string, message: string) => { const handleReviewPointStatusChange = async (reviewPointResultId: string, editAuditStatusId: string | number, newStatus: string, message: string) => {
// 将字符串的布尔值转换为布尔类型 // 将字符串的布尔值转换为布尔类型
@@ -569,53 +591,16 @@ export default function ReviewDetails() {
// 显示加载状态 // 显示加载状态
setIsLoading(true); setIsLoading(true);
// 使用 fetch 调用 action // 使用 Remix 的 useFetcher 调用 action
const formData = new FormData(); const formData = new FormData();
formData.append("intent", "confirmReviewResults"); formData.append("intent", "confirmReviewResults");
formData.append("documentId", document.id.toString()); formData.append("documentId", document.id.toString());
const response = await fetch(window.location.pathname, { fetcher.submit(formData, { method: "POST" });
method: "POST",
body: formData,
});
// 检查响应是否为JSON格式
const contentType = response.headers.get("content-type");
if (!contentType || !contentType.includes("application/json")) {
console.error('服务器返回了非JSON响应,状态码:', response.status);
const text = await response.text();
console.error('响应内容:', text.substring(0, 500));
if (response.status === 401) {
toastService.error('登录已过期,请重新登录');
window.location.href = '/login';
return;
} else if (response.status >= 500) {
toastService.error('服务器内部错误,请稍后重试');
return;
} else {
toastService.error('请求失败,请检查网络连接');
return;
}
}
const result = await response.json();
if (!result.success) {
console.error('确认评查结果失败:', result.error);
toastService.error(`确认评查结果失败: ${result.error}`);
return;
}
// 显示成功消息
toastService.success('评查结果已确认,文档审核状态已更新');
// 导航到文档列表页
navigate('/documents');
} catch (error) { } catch (error) {
console.error('确认评查结果出错:', error); console.error('确认评查结果出错:', error);
toastService.error(`确认评查结果失败: ${error instanceof Error ? error.message : '未知错误'}`); toastService.error(`确认评查结果失败: ${error instanceof Error ? error.message : '未知错误'}`);
} finally {
setIsLoading(false); setIsLoading(false);
} }
}; };