测通完成评查,投票,意见列表,任务列表,任务关联文档列表的内容。剩余创建任务,提出意见的完善
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
{"projectName":"trae_docreview_m5eu"}
|
||||||
@@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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':
|
||||||
// 进行中状态不执行操作
|
// 进行中状态不执行操作
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user