From 8800e982ab13d14f2cf68991183970298155ca73 Mon Sep 17 00:00:00 2001 From: yorn <1057707203@qq.com> Date: Wed, 23 Jul 2025 10:22:51 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B5=8B=E9=80=9A=E5=AE=8C=E6=88=90=E8=AF=84?= =?UTF-8?q?=E6=9F=A5=EF=BC=8C=E6=8A=95=E7=A5=A8=EF=BC=8C=E6=84=8F=E8=A7=81?= =?UTF-8?q?=E5=88=97=E8=A1=A8=EF=BC=8C=E4=BB=BB=E5=8A=A1=E5=88=97=E8=A1=A8?= =?UTF-8?q?=EF=BC=8C=E4=BB=BB=E5=8A=A1=E5=85=B3=E8=81=94=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E7=9A=84=E5=86=85=E5=AE=B9=E3=80=82=E5=89=A9?= =?UTF-8?q?=E4=BD=99=E5=88=9B=E5=BB=BA=E4=BB=BB=E5=8A=A1=EF=BC=8C=E6=8F=90?= =?UTF-8?q?=E5=87=BA=E6=84=8F=E8=A7=81=E7=9A=84=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vercel/project.json | 1 + app/api/cross-checking/cross-file-result.ts | 203 ++++++++++--- app/api/cross-checking/cross-files-upload.ts | 15 +- app/api/user/user-management.ts | 56 +++- .../cross-checking/DocumentListModal.tsx | 41 ++- .../cross-checking/ReviewPointsList.tsx | 236 ++++++++------- app/components/ui/MessageModal.tsx | 2 +- app/components/ui/Modal.tsx | 5 +- app/components/ui/Toast.tsx | 1 + app/routes/cross-checking._index.tsx | 17 +- app/routes/cross-checking.result.tsx | 151 +++++++--- app/routes/cross-checking.upload.tsx | 282 ++++++++++++++---- app/routes/reviews.tsx | 71 ++--- 13 files changed, 750 insertions(+), 331 deletions(-) create mode 100644 .vercel/project.json diff --git a/.vercel/project.json b/.vercel/project.json new file mode 100644 index 0000000..35ec181 --- /dev/null +++ b/.vercel/project.json @@ -0,0 +1 @@ +{"projectName":"trae_docreview_m5eu"} \ No newline at end of file diff --git a/app/api/cross-checking/cross-file-result.ts b/app/api/cross-checking/cross-file-result.ts index 8a649e3..1779e54 100644 --- a/app/api/cross-checking/cross-file-result.ts +++ b/app/api/cross-checking/cross-file-result.ts @@ -1,4 +1,24 @@ -// import { postgrestPost } from "../postgrest-client"; +import { postgrestGet, postgrestPut } from "../postgrest-client"; + +/** + * 从不同格式的 API 响应中提取数据 + * @param responseData API 响应数据 + * @returns 提取后的数据或 null + */ +function extractApiData(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[]; can_vote: boolean; problem_message: string; - // 兼容旧字段 - id?: string | number; - 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; + proposer_id: number; + created_at: string; } /** @@ -74,6 +81,34 @@ async function safeGetJWT(jwtToken?: string): Promise { return jwtToken || ''; } +/** + * 获取当前登录用户是否是发起人 + * @param taskId 任务ID + * @param userId 用户ID + * @returns 是否是发起人 + */ +export async function findIsProposer(taskId: string | number, userId: number | undefined): Promise { + // 通过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 意见数据 @@ -154,7 +189,7 @@ export async function getCrossCheckingOpinions( // 如果没传userId,默认用1 const realUserId = userId ?? 1; // 实际后端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', headers: { 'Content-Type': 'application/json', @@ -171,7 +206,7 @@ export async function getCrossCheckingOpinions( throw new Error('获取意见列表失败'); } const data = await response.json(); - + console.log('最原始的返回data', data); // 处理新的数据结构,支持分页 const responseData = data.data || data; const pagination = data.pagination; @@ -189,13 +224,8 @@ export async function getCrossCheckingOpinions( pending_voters?: string[]; can_vote?: boolean; problem_message?: string; - evaluation_point_id?: string | number; - document_id?: string | number; - status?: string; - created_at?: string; - updated_at?: string; - is_vote?: boolean; - current_user_is_proposer?: boolean; + proposer_id: number; + created_at: string; } // 适配后端返回结构,使用新字段 @@ -211,21 +241,8 @@ export async function getCrossCheckingOpinions( pending_voters: item.pending_voters || [], can_vote: item.can_vote ?? false, problem_message: item.problem_message || '', - // 兼容旧字段 - id: item.proposal_id, - 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 + proposer_id: item.proposer_id, + created_at: item.created_at })) : []; return { @@ -271,7 +288,8 @@ export interface OpinionActionRequest { */ export async function performOpinionAction( actionData: OpinionActionRequest, - jwtToken?: string + jwtToken?: string, + userInfo?: { user_id: number } ): Promise> { try { const token = await safeGetJWT(jwtToken); @@ -284,23 +302,23 @@ export async function performOpinionAction( case 'agree': message = '已赞同该意见'; 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; case 'disagree': message = '已反对该意见'; 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; case 'withdraw_vote': message = '已撤销投票'; // 撤销投票的接口,根据实际API调整 - endpoint = `${API_BASE_URL}/admin/cross_review/proposals/${actionData.opinionId}/votes/withdraw`; - requestBody = {}; + endpoint = `${API_BASE_URL}/admin/cross_review/proposals/${actionData.opinionId}/votes`; + requestBody = { vote_type: 'cancel', voter_id: userInfo?.user_id }; break; case 'withdraw_opinion': message = '已撤销意见'; // 撤销意见的接口,根据实际API调整 - endpoint = `${API_BASE_URL}/admin/cross_review/proposals/${actionData.opinionId}/withdraw`; + endpoint = `${API_BASE_URL}/admin/cross_review/proposals/${actionData.opinionId}`; requestBody = {}; break; default: @@ -308,7 +326,7 @@ export async function performOpinionAction( } const response = await fetch(endpoint, { - method: 'POST', + method: actionData.action === 'withdraw_opinion' ? 'DELETE' : 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` @@ -318,6 +336,8 @@ export async function performOpinionAction( const data = await response.json(); + console.log('返回的意见列表数据',data); + if (!response.ok) { 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(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 + }; + } +} + diff --git a/app/api/cross-checking/cross-files-upload.ts b/app/api/cross-checking/cross-files-upload.ts index 5d82220..0b628bf 100644 --- a/app/api/cross-checking/cross-files-upload.ts +++ b/app/api/cross-checking/cross-files-upload.ts @@ -104,7 +104,8 @@ export async function uploadCrossCheckingDocument( remark: string = '', isTestDocument: boolean = false, 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}> { try { console.log('【交叉评查上传】开始上传文档:', { fileName, fileSize: binaryData.byteLength, typeId }); @@ -140,11 +141,17 @@ export async function uploadCrossCheckingDocument( // 发送请求 try { console.log('【交叉评查上传】开始fetch请求...'); + const headers: HeadersInit = { + 'X-File-Name': encodeURIComponent(fileName), + }; + + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + const response = await fetch(uploadUrl, { method: 'POST', - headers: { - 'X-File-Name': encodeURIComponent(fileName) - }, + headers, body: formData }); diff --git a/app/api/user/user-management.ts b/app/api/user/user-management.ts index f3c2026..0958f90 100644 --- a/app/api/user/user-management.ts +++ b/app/api/user/user-management.ts @@ -1,4 +1,5 @@ import { get } from '../axios-client'; +import { API_BASE_URL } from '../../config/api-config'; // 用户信息接口 export interface UserInfo { @@ -48,27 +49,52 @@ export interface ApiResponse { * @param includeUsers 是否包含用户信息 * @returns 组织架构树 */ -export async function getOrganizationTree(includeUsers: boolean = true): Promise> { +export async function getOrganizationTree(includeUsers: boolean = true, jwtToken?: string): Promise> { try { console.log('开始调用获取组织架构API'); - - const response = await get( - `/admin/users/organizations?include_users=${includeUsers}` - ); - console.log('组织架构API响应:', response); - - if (response.error) { - console.error('获取组织架构失败:', response.error); - return { - success: false, - error: response.error - }; + let responseData: OrganizationResponse; + + if (jwtToken) { + // 如果提供了JWT Token,则使用fetch并携带Authorization头 + const url = `${API_BASE_URL}/admin/users/organizations?include_users=${includeUsers}`; + const response = await fetch(url, { + 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( + `/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 { success: true, - data: response.data + data: responseData }; } catch (error) { console.error('获取组织架构失败:', error); @@ -211,4 +237,4 @@ export async function getFlatOrganizations(includeUsers: boolean = true): Promis error: error instanceof Error ? error.message : '获取扁平化组织列表失败' }; } -} \ No newline at end of file +} \ No newline at end of file diff --git a/app/components/cross-checking/DocumentListModal.tsx b/app/components/cross-checking/DocumentListModal.tsx index 8415693..bc16f32 100644 --- a/app/components/cross-checking/DocumentListModal.tsx +++ b/app/components/cross-checking/DocumentListModal.tsx @@ -65,6 +65,31 @@ export function DocumentListModal({ onViewFile(fileId); } }; + + // 审核状态选项及样式 - 与documents._index.tsx保持一致 + const auditStatusMapping: Record = { + "-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 ( +
+ + {statusInfo.label} +
+ ); + }; + // 渲染问题摘要 const renderIssues = (file: TaskDocument) => { @@ -143,7 +168,7 @@ export function DocumentListModal({ { title: "文件类型", key: "fileType", - width: "10%", + width: "8%", render: (_: unknown, file: TaskDocument) => ( { const uploadTime = formatDate(file.upload_time).split(' '); const date = uploadTime[0]; @@ -175,7 +200,7 @@ export function DocumentListModal({ { title: "评查统计", key: "reviewStatus", - width: "12%", + width: "10%", render: (_: unknown, file: TaskDocument) => // 要文件切分处理完之后,再显示评查统计 file.status === 'Processed' ? ( @@ -225,7 +250,7 @@ export function DocumentListModal({ key: "score", width: "8%", render: (_: unknown, file: TaskDocument) => ( -
+
{file.final_score ? ( = 90 ? 'text-green-600' : @@ -240,6 +265,12 @@ export function DocumentListModal({
) }, + { + title: '审核状态', + key: 'auditStatus', + width: '8%', + render: (_: unknown, file: TaskDocument) => renderAuditStatus(file) + }, { title: "问题摘要", key: "issues", @@ -322,4 +353,4 @@ export function DocumentListModal({
); -} \ No newline at end of file +} \ No newline at end of file diff --git a/app/components/cross-checking/ReviewPointsList.tsx b/app/components/cross-checking/ReviewPointsList.tsx index e087ccd..2e58f27 100644 --- a/app/components/cross-checking/ReviewPointsList.tsx +++ b/app/components/cross-checking/ReviewPointsList.tsx @@ -30,7 +30,7 @@ import { type CrossCheckingOpinion, type OpinionActionType } 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'; /** @@ -159,6 +159,11 @@ interface ScoringProposal { document_id: string | number; } +interface UserInfo { + id: number; + [key: string]: unknown; +} + interface ReviewPointsListProps { reviewPoints: ReviewPoint[]; statistics: Statistics; @@ -167,6 +172,7 @@ interface ReviewPointsListProps { onStatusChange?: (id: string, editAuditStatusId: string | number, status: string, message: string) => void; scoringProposals?: ScoringProposal[]; jwtToken?: string; // 添加JWT token参数 + userInfo?: UserInfo; // 添加用户信息参数 } /** @@ -424,7 +430,8 @@ export function ReviewPointsList({ activeReviewPointResultId, onReviewPointSelect, scoringProposals = [], - jwtToken + jwtToken, + userInfo }: ReviewPointsListProps) { // 状态管理 const [searchText, setSearchText] = useState(''); // 搜索文本 @@ -436,7 +443,7 @@ export function ReviewPointsList({ // 将来可以用于显示相关的评分提案信息 useEffect(() => { if (scoringProposals && scoringProposals.length > 0) { - console.log('收到评分提案数据:', scoringProposals.length, '个提案'); + // console.log('收到评分提案数据:', scoringProposals.length, '个提案'); // 获取提案的evaluation_result_id const evaluationResultIds = scoringProposals.map(proposal => Number(proposal.evaluation_result_id)); setEvaluationResultIds(evaluationResultIds); @@ -471,27 +478,31 @@ export function ReviewPointsList({ // 监听fetcher状态变化 - 获取意见列表数据 useEffect(() => { if (fetcher.data && fetcher.state === 'idle' && opinionListLoading) { - const data = fetcher.data as { - success?: boolean; - data?: { - opinions: CrossCheckingOpinion[]; - total: number; - }; - error?: string; + const data = fetcher.data as { + success?: boolean; + data?: { + opinions: CrossCheckingOpinion[]; + total: number; + pagination?: { + page: number; + page_size: number; + total: number; + total_pages: number; + }; + }; + error?: string; }; - if (data.success && data.data) { - console.log('意见列表数据', data.data); + console.log('data.data', data.data); setOpinionListData(data.data.opinions || []); setOpinionListTotal(data.data.total || 0); - // 使用当前状态值而不是依赖项中的值 - setOpinionListCurrentPage(prev => prev); - setOpinionListPageSize(prev => prev); + if (data.data.pagination) { + setOpinionListCurrentPage(data.data.pagination.page); + setOpinionListPageSize(data.data.pagination.page_size); + } } else { - console.error('加载意见列表失败:', data.error); toastService.error(data.error || '加载意见列表失败'); } - setOpinionListLoading(false); } }, [fetcher.data, fetcher.state, opinionListLoading]); @@ -568,12 +579,11 @@ export function ReviewPointsList({ const loadOpinionListData = async (page: number = 1, pageSize: number = 10, documentId?: string | number) => { // 使用传入的documentId或者从selectedReviewPoint获取 const targetDocumentId = documentId || selectedReviewPoint?.documentId; - console.log('加载意见列表数据', targetDocumentId); + if (!targetDocumentId) return; setOpinionListLoading(true); try { - console.log('加载意见列表数据', targetDocumentId, page, pageSize); // 使用 fetcher 调用路由的 action const formData = new FormData(); @@ -595,8 +605,8 @@ export function ReviewPointsList({ * 打开意见列表模态框 */ const handleOpenOpinionListModal = (reviewPoint: ReviewPoint) => { - console.log('查看reviewPoints', reviewPoints); - if (scoringProposals.length+1 === 0) { + console.log('查看reviewPoint', reviewPoint); + if (scoringProposals.length === 0) { toastService.warning('当前文件尚未有人提出过意见'); return; } @@ -626,7 +636,7 @@ export function ReviewPointsList({ setPerformingAction(actionKey); try { - const response = await performOpinionAction({ opinionId, action }, jwtToken); + const response = await performOpinionAction({ opinionId, action }, jwtToken, userInfo as { user_id: number } | undefined); if (response.error) { toastService.error(response.error); @@ -634,12 +644,14 @@ export function ReviewPointsList({ } toastService.success(response.data?.message || '操作成功'); + + // console.log('即将重新加载数据'); // 重新加载数据 await loadOpinionListData(opinionListCurrentPage, opinionListPageSize); } catch (error) { console.error('操作失败:', error); - toastService.error('操作失败,请稍后重试'); + toastService.error(error instanceof Error ? error.message : '操作失败,请稍后重试'); } finally { setPerformingAction(null); } @@ -649,6 +661,7 @@ export function ReviewPointsList({ * 处理意见列表分页变化 */ const handleOpinionListPageChange = (page: number) => { + setOpinionListCurrentPage(page); loadOpinionListData(page, opinionListPageSize); }; @@ -656,6 +669,7 @@ export function ReviewPointsList({ * 处理意见列表每页大小变化 */ const handleOpinionListPageSizeChange = (size: number) => { + setOpinionListPageSize(size); loadOpinionListData(1, size); }; @@ -2550,7 +2564,7 @@ export function ReviewPointsList({ { title: "问题描述", key: "problem_message", - width: "20%", + width: "18%", render: (_: unknown, record: CrossCheckingOpinion) => (
{record.problem_message}
) @@ -2566,7 +2580,7 @@ export function ReviewPointsList({ { title: "调整分数", key: "proposed_score", - width: "8%", + width: "5%", align: "center" as const, render: (_: unknown, record: CrossCheckingOpinion) => ( = 0 ? 'text-green-600' : 'text-red-600'}`}> @@ -2576,8 +2590,8 @@ export function ReviewPointsList({ }, { title: "投票人", - key: "voter_count", - width: "8%", + key: "votes", + width: "25%", align: "center" as const, render: (_: unknown, record: CrossCheckingOpinion) => { // 投票类型配置 @@ -2604,7 +2618,6 @@ export function ReviewPointsList({ border: "border border-gray-200" } ]; - return (
{voterGroups.map((group) => ( @@ -2633,7 +2646,7 @@ export function ReviewPointsList({ { title: "意见发起人", key: "proposer", - width: "10%", + width: "4%", render: (_: unknown, record: CrossCheckingOpinion) => (
) }, + { + title: "发起时间", + key: "created_at", + width: "18%", + render: (_: unknown, record: CrossCheckingOpinion) => ( +
{record.created_at}
+ ) + }, { title: "操作", key: "operation", @@ -2652,19 +2673,19 @@ export function ReviewPointsList({ render: (_: unknown, record: CrossCheckingOpinion) => { const isPerforming = (action: string) => performingAction === `${record.proposal_id}-${action}`; return ( - + ); } } ]} dataSource={opinionListData} - rowKey="id" + rowKey="proposal_id" emptyText="暂无意见数据" className="opinion-list-table" /> {/* 分页组件 */} - {opinionListTotal > opinionListPageSize && ( + {opinionListTotal > 0 && ( )} @@ -2686,27 +2707,24 @@ export function ReviewPointsList({ } // 操作按钮区美化+弹窗确认组件 -function OpinionActions({ record, isPerforming, handleOpinionAction }: { +function OpinionActions({ record, isPerforming, handleOpinionAction, userInfo }: { record: CrossCheckingOpinion; isPerforming: (action: string) => boolean; handleOpinionAction: (id: string | number, action: OpinionActionType) => void; + userInfo?: { user_id: number }; }) { - const canVote = record.can_vote !== false; - const [showWithdrawModal, setShowWithdrawModal] = useState(false); - const [withdrawType, setWithdrawType] = useState<'withdraw_vote' | 'withdraw_opinion' | null>(null); + const [showModal, setShowModal] = useState(null); const [countdown, setCountdown] = useState(3); const [counting, setCounting] = useState(false); - const handleWithdraw = (type: 'withdraw_vote' | 'withdraw_opinion') => { - setWithdrawType(type); - setShowWithdrawModal(true); - setCountdown(3); - setCounting(true); - }; - useEffect(() => { let timer: NodeJS.Timeout; - if (showWithdrawModal && counting && countdown > 0) { + if ( + showModal && + (showModal === 'withdraw_opinion' || showModal === 'withdraw_vote') && + counting && + countdown > 0 + ) { timer = setTimeout(() => { setCountdown((c) => c - 1); }, 1000); @@ -2714,89 +2732,108 @@ function OpinionActions({ record, isPerforming, handleOpinionAction }: { setCounting(false); } return () => clearTimeout(timer); - }, [showWithdrawModal, counting, countdown]); + }, [showModal, counting, countdown]); - const handleWithdrawConfirm = () => { - if (withdrawType && countdown === 0) { - handleOpinionAction(record.proposal_id, withdrawType); - setShowWithdrawModal(false); - setWithdrawType(null); + const handleConfirm = () => { + if (showModal === 'withdraw_opinion' || showModal === 'withdraw_vote') { + if (countdown === 0) { + handleOpinionAction(record.proposal_id, showModal); + setShowModal(null); + setCountdown(3); + setCounting(false); + } + } else { + // 赞同/反对等操作直接执行 + handleOpinionAction(record.proposal_id, showModal!); + setShowModal(null); setCountdown(3); setCounting(false); } }; - const handleWithdrawCancel = () => { - setShowWithdrawModal(false); - setWithdrawType(null); + const handleCancel = () => { + setShowModal(null); setCountdown(3); setCounting(false); }; + + // 判断是否是发起人 + const isProposer = userInfo && record.proposer_id === userInfo.user_id; + return (
- - - {(!canVote || record.is_vote) && ( + {/* 仅当can_vote为true时显示赞同/反对按钮 */} + {record.can_vote && ( + <> + + + + )} + {/* 仅当can_vote为false时显示撤销投票按钮 */} + {!record.can_vote && ( )} - {record.current_user_is_proposer && ( + {/* 仅当是发起人才显示撤销意见按钮 */} + {isProposer && ( )} - {showWithdrawModal && ( + {/* 确认操作模态框 */} + {showModal && (
} >
-
确定要撤销此操作吗?
+
确定要进行此操作吗?
评查点:{record.evaluation_point_name || record.proposal_id}
@@ -2820,6 +2857,8 @@ const openResultModal = (recordId: string) => { // 交叉评查记录操作按钮组件 export function ActionButtons({ record }: { record: CrossCheckingRecord }) { + const navigate = useNavigate(); + // 根据记录状态确定按钮类型 const getButtonConfig = () => { switch (record.status) { @@ -2828,22 +2867,14 @@ export function ActionButtons({ record }: { record: CrossCheckingRecord }) { text: '去评查', bgColor: 'bg-blue-600', hoverColor: 'hover:bg-blue-700', - icon: ( - - - - ) + icon: }; case 'in_progress': return { text: '进行中', bgColor: 'bg-gray-500', hoverColor: 'hover:bg-gray-600', - icon: ( - - - - ) + icon: }; case 'completed': default: @@ -2851,23 +2882,22 @@ export function ActionButtons({ record }: { record: CrossCheckingRecord }) { text: '查看结果', bgColor: 'bg-green-600', hoverColor: 'hover:bg-green-700', - icon: ( - - - - ) + icon: }; } }; const buttonConfig = getButtonConfig(); - // 处理按钮点击事件 + /** + * 处理按钮点击事件 + * 使用React Router的navigate方法替代window.location.href,避免页面刷新 + */ const handleAction = () => { switch (record.status) { case 'pending': - // 跳转到评查页面 - window.location.href = `/review/${record.id}`; + // 使用navigate跳转到评查页面,避免页面刷新 + navigate(`/review/${record.id}`); break; case 'in_progress': // 进行中状态不执行操作 diff --git a/app/components/ui/MessageModal.tsx b/app/components/ui/MessageModal.tsx index 463082c..169c80b 100644 --- a/app/components/ui/MessageModal.tsx +++ b/app/components/ui/MessageModal.tsx @@ -180,7 +180,7 @@ export function MessageModal({ )} -
+
{message}
diff --git a/app/components/ui/Modal.tsx b/app/components/ui/Modal.tsx index 7734ab0..066a8f4 100644 --- a/app/components/ui/Modal.tsx +++ b/app/components/ui/Modal.tsx @@ -1,5 +1,6 @@ // app/components/ui/Modal.tsx import React, { useEffect, useRef } from 'react'; +import ReactDOM from 'react-dom'; import modalStyles from '~/styles/components/modal.css?url'; // 导出样式 @@ -102,7 +103,7 @@ export function Modal({ if (!isOpen) return null; - return ( + const modalNode = ( ); + + return ReactDOM.createPortal(modalNode, document.body); } \ No newline at end of file diff --git a/app/components/ui/Toast.tsx b/app/components/ui/Toast.tsx index bbebed3..9f199d8 100644 --- a/app/components/ui/Toast.tsx +++ b/app/components/ui/Toast.tsx @@ -202,6 +202,7 @@ export function Toast({ aria-live="polite" onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} + style={{ zIndex: 99999, position: 'relative' }} >
diff --git a/app/routes/cross-checking._index.tsx b/app/routes/cross-checking._index.tsx index 8f5ca4e..89674d2 100644 --- a/app/routes/cross-checking._index.tsx +++ b/app/routes/cross-checking._index.tsx @@ -77,6 +77,8 @@ export async function loader({ request }: LoaderFunctionArgs) { getCrossCheckingTasks(params, userInfo, frontendJWT), getCrossCheckingStats(userInfo, frontendJWT) ]); + + console.log('tasksResponse', tasksResponse.data?.tasks); if (!tasksResponse.success) { 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 => ({ @@ -274,12 +276,13 @@ export default function CrossCheckingIndex() { // 处理文档查看 - 导航到评查详情页 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<{ taskId: number; + taskName: string; } | null>(null); // 加载分页数据 @@ -348,7 +351,7 @@ export default function CrossCheckingIndex() { type="primary" size="small" className="operation-btn primary" - onClick={() => handleViewResult(task.id)} + onClick={() => handleViewResult(task.id,task.taskName)} > 去评查 @@ -360,7 +363,7 @@ export default function CrossCheckingIndex() { type="default" size="small" className="operation-btn secondary" - onClick={() => handleViewResult(task.id)} + onClick={() => handleViewResult(task.id,task.taskName)} > 进行中 @@ -372,7 +375,7 @@ export default function CrossCheckingIndex() { type="default" size="small" className="operation-btn secondary" - onClick={() => handleViewResult(task.id)} + onClick={() => handleViewResult(task.id,task.taskName)} > 查看结果 @@ -494,7 +497,7 @@ export default function CrossCheckingIndex() { setModalState(prev => ({ ...prev, loading: false, - title: `任务 ${currentTaskInfo?.taskId || ''} - 文档列表`, + title: `${currentTaskInfo?.taskName || ''} - 文档列表`, files: files || [], total: total || 0, currentPage: currentPage || prev.currentPage, diff --git a/app/routes/cross-checking.result.tsx b/app/routes/cross-checking.result.tsx index a962eaa..43d60bf 100644 --- a/app/routes/cross-checking.result.tsx +++ b/app/routes/cross-checking.result.tsx @@ -23,11 +23,12 @@ */ 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 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 { confirmReviewResults, checkProposalVotes, findIsProposer } from "~/api/cross-checking/cross-file-result"; // 导入交叉评查详情页面组件 import { @@ -183,19 +184,26 @@ export async function loader({ request }: LoaderFunctionArgs) { try { const url = new URL(request.url); const id = url.searchParams.get('id') || undefined; + const taskId = url.searchParams.get('tId') || undefined; const previousRoute = url.searchParams.get('previousRoute') || ''; // console.log("id-------",id); if (!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 { frontendJWT } = await getUserSession(request); + const { userInfo, frontendJWT } = await getUserSession(request); // 获取评查点数据,传递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("reviewData-------",JSON.stringify('data' in reviewData ? reviewData.data : '',null,2)); // console.log("reviewData-------",JSON.stringify(reviewData,null,2)); @@ -215,7 +223,9 @@ export async function loader({ request }: LoaderFunctionArgs) { statistics: reviewData.stats, comparison_document: reviewData.comparison_document, scoring_proposals: reviewData.scoring_proposals || [], - jwtToken: frontendJWT // 传递JWT token + userInfo: userInfo, + jwtToken: frontendJWT, // 传递JWT token + isProposer: isProposer }); } else { console.error("返回的评查数据格式不正确",JSON.stringify(reviewData,null,2)); @@ -298,20 +308,9 @@ export async function action({ request }: ActionFunctionArgs) { } if (intent === "confirmReviewResults") { - toastService.error('确认评查结果功能暂未实现'); - // TODO 应该在cross-file-result.ts中新增一个确认的方法 - // 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 }); + // 检查文档下提案是否存在未投票用户,首先先打开一个模态框,提示用户是否确认完成评查,如果用户点击确认,则调用confirmReviewResults接口,如果用户点击取消,则关闭模态框 + // 模态框内的数据需要根据checkProposalVotes返回回来的数据进行显示,如果存在未投票用户,则提示用户存在未投票用户,如果不存在未投票用户,则提示用户完成评查 } - - return Response.json({ success: false, error: "未知的操作类型" }, { status: 400 }); } catch (error) { console.error('Action处理失败:', error); return Response.json({ @@ -324,7 +323,7 @@ export async function action({ request }: ActionFunctionArgs) { export default function CrossCheckingResult() { const navigate = useNavigate(); const loaderData = useLoaderData(); - 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 [reviewData, setReviewData] = useState(null); const [activeReviewPointResultId, setActiveReviewPointResultId] = useState(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) { toastService.error('文档数据不完整,无法确认评查结果'); return; } try { - // 显示加载状态 setIsLoading(true); - // 使用 fetch 调用 action - const formData = new FormData(); - formData.append("intent", "confirmReviewResults"); - formData.append("documentId", document.id.toString()); - - const response = await fetch(window.location.pathname, { - method: "POST", - body: formData, - }); - - const result = await response.json(); + // 1. 先检查未投票 + const checkRes = await checkProposalVotes(document.id, jwtToken); + console.log("checkRes", checkRes); - if (!result.success) { - console.error('确认评查结果失败:', result.error); - toastService.error(`确认评查结果失败: ${result.error}`); + if (checkRes.error) { + toastService.error(checkRes.error); + setIsLoading(false); return; } - // 显示成功消息 - toastService.success('评查结果已确认,文档审核状态已更新'); + // 2. 解析返回数据,定义明确的类型 + 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); + } catch (error) { + setIsLoading(false); + toastService.error(`确认评查结果失败: ${error instanceof Error ? error.message : '未知错误'}`); } }; @@ -608,13 +653,20 @@ export default function CrossCheckingResult() {
{/* 完成评查按钮 */} - + {isProposer && ( + + )}
{/* 文件信息和操作按钮 */} @@ -648,6 +700,7 @@ export default function CrossCheckingResult() { onStatusChange={handleReviewPointStatusChange} scoringProposals={scoring_proposals as ScoringProposal[]} jwtToken={jwtToken} + userInfo={userInfo} />
diff --git a/app/routes/cross-checking.upload.tsx b/app/routes/cross-checking.upload.tsx index 3467c4d..68cf172 100644 --- a/app/routes/cross-checking.upload.tsx +++ b/app/routes/cross-checking.upload.tsx @@ -1,6 +1,6 @@ import { useState, useRef, useEffect } from "react"; -import { type MetaFunction, type ActionFunctionArgs } from "@remix-run/node"; -import { Form, useNavigation, useNavigate } from "@remix-run/react"; +import { type MetaFunction, type ActionFunctionArgs, type LoaderFunctionArgs, json } from "@remix-run/node"; +import { Form, useNavigate, useLoaderData } from "@remix-run/react"; import { UploadArea, type UploadAreaRef } from "~/components/ui/UploadArea"; import { Button } from "~/components/ui/Button"; import { messageService } from "~/components/ui/MessageModal"; @@ -124,6 +124,60 @@ const TreeNodeCheckbox: React.FC<{
); }; +/** + * 获取用户会话和前端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) => { const formData = await request.formData(); const caseType = formData.get("caseType") as string; @@ -137,10 +191,15 @@ export const action = async ({ request }: ActionFunctionArgs) => { }; export default function CrossCheckingUpload() { + // 获取loader数据 + const { userInfo, frontendJWT } = useLoaderData(); + // 基础状态 const [caseType, setCaseType] = useState(CaseType.ADMINISTRATIVE_PENALTY); // 步骤状态 const [currentStep, setCurrentStep] = useState(1); + // 任务创建状态 + const [isCreatingTask, setIsCreatingTask] = useState(false); // 步骤1:任务信息 const [taskInfo, setTaskInfo] = useState({ name: '', @@ -171,8 +230,7 @@ export default function CrossCheckingUpload() { const singleUploadRef = useRef(null); const multipleUploadRef = useRef(null); - // 获取当前typeId - const currentTypeId = CASE_TYPE_TO_TYPE_ID[caseType]; + // 处理案卷类型切换 const handleCaseTypeChange = (type: CaseType) => { @@ -330,71 +388,177 @@ export default function CrossCheckingUpload() { 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; - if (filesToUpload.length === 0) { toastService.error("请先选择要上传的文件"); return; } + setIsCreatingTask(true); setIsUploading(true); try { - console.log("开始批量上传文件:", filesToUpload.length, "个,案卷类型:", caseType, "typeId:", currentTypeId); + // 第一步:上传文件 + console.log("开始批量上传文件:", filesToUpload.length, "个,案卷类型:", caseType); - const result = await batchUploadCrossCheckingFiles( - filesToUpload, - currentTypeId, + const uploadResult = await batchUploadCrossCheckingFiles( + filesToUpload.map(f => f.file), + caseType, priority, - documentNumber, - remark, - isTestDocument + isTestDocument, + frontendJWT ); - const { successes, failures } = result; + const { successes, failures } = uploadResult; - 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: '确定', - } - ); + if (failures.length > 0) { + toastService.error(`文件上传失败:${failures[0].error}`); + return; } + // 第二步:创建交叉评查任务 + 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) { - console.error("批量上传失败:", error); - toastService.error("文件上传过程中发生错误"); - messageService.error(`文件上传失败:${error instanceof Error ? error.message : '未知错误'}`, { - title: '上传失败', + console.error("创建任务失败:", error); + toastService.error(`创建任务失败:${error instanceof Error ? error.message : '未知错误'}`); + messageService.error(`创建任务失败:${error instanceof Error ? error.message : '未知错误'}`, { + title: '创建失败', confirmText: '确定', }); } finally { + setIsCreatingTask(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 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 navigation = useNavigation(); - const isSubmitting = navigation.state === "submitting"; + // const navigation = useNavigation(); + // 由于 isSubmitting 未被使用,暂时移除该行代码 const navigate = useNavigate(); @@ -419,7 +583,8 @@ export default function CrossCheckingUpload() { try { console.log('开始加载组织架构数据'); - const response = await getOrganizationTree(true); + // 传递JWT token到API调用 + const response = await getOrganizationTree(true, frontendJWT); if (response.success && response.data) { console.log('原始API数据:', response.data); @@ -803,10 +968,10 @@ export default function CrossCheckingUpload() { @@ -819,16 +984,21 @@ export default function CrossCheckingUpload() { )} - {/* 上传进度提示 */} - {isUploading && ( + {/* 创建任务进度提示 */} + {(isUploading || isCreatingTask) && (
- - 正在上传文件... + + + {isCreatingTask ? "正在创建任务..." : "正在上传文件..."} +

- 正在上传 {uploadType === 'single' ? singleFiles.length : multipleFiles.length} 个文件,请稍候 + {isCreatingTask + ? `正在创建交叉评查任务:${taskInfo.name}` + : `正在上传 ${uploadType === 'single' ? singleFiles.length : multipleFiles.length} 个文件,请稍候` + }

diff --git a/app/routes/reviews.tsx b/app/routes/reviews.tsx index 7d191e6..3357f55 100644 --- a/app/routes/reviews.tsx +++ b/app/routes/reviews.tsx @@ -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: true, data: response.data }); + return Response.json({ success: true, data: response.data, intent: "confirmReviewResults" }); } catch (updateError) { console.error('调用updateReviewResult时发生异常:', updateError); return Response.json({ @@ -258,15 +258,16 @@ export async function action({ request }: ActionFunctionArgs) { if (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) { console.error('调用confirmReviewResults时发生异常:', confirmError); return Response.json({ success: false, - error: confirmError instanceof Error ? confirmError.message : '确认评查结果时发生未知错误' + error: confirmError instanceof Error ? confirmError.message : '确认评查结果时发生未知错误', + intent: "confirmReviewResults" }, { status: 500 }); } } @@ -522,6 +523,27 @@ export default function ReviewDetails() { } }, [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) => { // 将字符串的布尔值转换为布尔类型 @@ -569,53 +591,16 @@ export default function ReviewDetails() { // 显示加载状态 setIsLoading(true); - // 使用 fetch 调用 action + // 使用 Remix 的 useFetcher 调用 action const formData = new FormData(); formData.append("intent", "confirmReviewResults"); formData.append("documentId", document.id.toString()); - const response = await fetch(window.location.pathname, { - 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(); + fetcher.submit(formData, { method: "POST" }); - if (!result.success) { - console.error('确认评查结果失败:', result.error); - toastService.error(`确认评查结果失败: ${result.error}`); - return; - } - - // 显示成功消息 - toastService.success('评查结果已确认,文档审核状态已更新'); - - // 导航到文档列表页 - navigate('/documents'); } catch (error) { console.error('确认评查结果出错:', error); toastService.error(`确认评查结果失败: ${error instanceof Error ? error.message : '未知错误'}`); - } finally { setIsLoading(false); } };