From 4d5ec6cdb753e2c1a450193970253c7bfc433c7f Mon Sep 17 00:00:00 2001 From: Wren Date: Sun, 20 Jul 2025 21:29:42 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8E=A5=E5=85=A5feat(cross-checking):=20?= =?UTF-8?q?=E6=95=B4=E5=90=88=E7=BB=84=E7=BB=87=E6=9E=B6=E6=9E=84=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=B9=B6=E4=BC=98=E5=8C=96=E6=84=8F=E8=A7=81=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新 API 配置,使用新的后端服务地址- 移除前端模拟数据,改为从后端获取真实数据- 优化意见列表接口,支持分页和用户身份验证 - 调整前端界面,适应新的数据结构和功能需求 --- app/api/cross-checking/cross-file-result.ts | 297 +++++------- app/api/cross-checking/cross-files.ts | 227 ++++++++- .../cross-checking/DocumentListModal.tsx | 4 +- .../cross-checking/ReviewPointsList.tsx | 371 ++++++++++++--- app/config/api-config.ts | 4 +- app/routes/cross-checking._index.tsx | 4 +- app/routes/cross-checking.upload.tsx | 448 +++++------------- 7 files changed, 758 insertions(+), 597 deletions(-) diff --git a/app/api/cross-checking/cross-file-result.ts b/app/api/cross-checking/cross-file-result.ts index e7bdfd0..575d804 100644 --- a/app/api/cross-checking/cross-file-result.ts +++ b/app/api/cross-checking/cross-file-result.ts @@ -1,5 +1,4 @@ // import { postgrestPost } from "../postgrest-client"; -import { API_BASE_URL } from "../../config/api-config"; /** * 提出意见的请求参数接口 @@ -29,20 +28,32 @@ export interface SubmitOpinionResponse { * 交叉评查意见数据接口 */ export interface CrossCheckingOpinion { - 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; + proposal_id: string | number; + evaluation_point_name: string; + proposed_score: number; + reason: string; + proposer: string; + votes: Array<{ voter: string; vote_type: string }>; + agree_voters: string[]; + disagree_voters: string[]; + 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; // 当前用户是否为意见发起人 + is_vote?: boolean; + voter_count?: number; + proposer_name?: string; + current_user_is_proposer?: boolean; } /** @@ -110,191 +121,95 @@ export async function submitCrossCheckingOpinion( * @param pageSize 每页大小 * @returns 意见列表和总数 */ +import { API_BASE_URL } from '../../config/api-config'; + export async function getCrossCheckingOpinions( documentId: string | number, page: number = 1, - pageSize: number = 10 + pageSize: number = 10, + userId?: number // 可选,便于后端接口对接 ): Promise> { try { - // 模拟数据 - 后续替换为真实API调用 - const mockOpinions: CrossCheckingOpinion[] = [ - { - id: 1, - evaluation_point_id: 101, - document_id: documentId, - audit_point: "合同主体信息核查", - found_issue: "合同签署方信息不完整", - audit_opinion: "合同中缺少乙方的详细联系方式,建议补充完整的地址和联系电话", - deduction_score: -2, - status: "pending", - created_at: "2024-01-15 10:30:00", - is_vote: false, - voter_count: 3, - proposer_name: "张三", - current_user_is_proposer: false + // 如果没传userId,默认用1 + const realUserId = userId ?? 1; + // 实际后端API调用,拼接API_BASE_URL + const response = await fetch(`${API_BASE_URL}/admin/cross_review/proposals/details`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', }, - { - id: 2, - evaluation_point_id: 102, - document_id: documentId, - audit_point: "合同金额核查", - found_issue: "合同金额与预算不符", - audit_opinion: "合同总金额超出预算范围,需要重新评估或调整预算", - deduction_score: -5, - status: "approved", - created_at: "2024-01-14 14:20:00", - is_vote: true, - voter_count: 5, - proposer_name: "李四", - current_user_is_proposer: true - }, - { - id: 3, - evaluation_point_id: 103, - document_id: documentId, - audit_point: "合同条款审查", - found_issue: "违约责任条款不明确", - audit_opinion: "合同中违约责任的具体计算方式和赔偿标准需要进一步明确", - deduction_score: -3, - status: "rejected", - created_at: "2024-01-13 09:15:00", - is_vote: false, - voter_count: 2, - proposer_name: "王五", - current_user_is_proposer: false - }, - { - id: 4, - evaluation_point_id: 103, - document_id: documentId, - audit_point: "合同条款审查", - found_issue: "违约责任条款不明确", - audit_opinion: "合同中违约责任的具体计算方式和赔偿标准需要进一步明确", - deduction_score: -3, - status: "rejected", - created_at: "2024-01-13 09:15:00", - is_vote: false, - voter_count: 2, - proposer_name: "王五", - current_user_is_proposer: false - }, - { - id: 5, - evaluation_point_id: 103, - document_id: documentId, - audit_point: "合同条款审查", - found_issue: "违约责任条款不明确", - audit_opinion: "合同中违约责任的具体计算方式和赔偿标准需要进一步明确", - deduction_score: -3, - status: "rejected", - created_at: "2024-01-13 09:15:00", - is_vote: false, - voter_count: 2, - proposer_name: "王五", - current_user_is_proposer: false - }, - { - id: 6, - evaluation_point_id: 103, - document_id: documentId, - audit_point: "合同条款审查", - found_issue: "违约责任条款不明确", - audit_opinion: "合同中违约责任的具体计算方式和赔偿标准需要进一步明确", - deduction_score: -3, - status: "rejected", - created_at: "2024-01-13 09:15:00", - is_vote: false, - voter_count: 2, - proposer_name: "王五", - current_user_is_proposer: false - }, - { - id: 7, - evaluation_point_id: 103, - document_id: documentId, - audit_point: "合同条款审查", - found_issue: "违约责任条款不明确", - audit_opinion: "合同中违约责任的具体计算方式和赔偿标准需要进一步明确", - deduction_score: -3, - status: "rejected", - created_at: "2024-01-13 09:15:00", - is_vote: false, - voter_count: 2, - proposer_name: "王五", - current_user_is_proposer: false - }, - { - id: 8, - evaluation_point_id: 103, - document_id: documentId, - audit_point: "合同条款审查", - found_issue: "违约责任条款不明确", - audit_opinion: "合同中违约责任的具体计算方式和赔偿标准需要进一步明确", - deduction_score: -3, - status: "rejected", - created_at: "2024-01-13 09:15:00", - is_vote: false, - voter_count: 2, - proposer_name: "王五", - current_user_is_proposer: false - }, - { - id: 9, - evaluation_point_id: 103, - document_id: documentId, - audit_point: "合同条款审查", - found_issue: "违约责任条款不明确", - audit_opinion: "合同中违约责任的具体计算方式和赔偿标准需要进一步明确", - deduction_score: -3, - status: "rejected", - created_at: "2024-01-13 09:15:00", - is_vote: false, - voter_count: 2, - proposer_name: "王五", - current_user_is_proposer: false - }, - { - id: 10, - evaluation_point_id: 103, - document_id: documentId, - audit_point: "合同条款审查", - found_issue: "违约责任条款不明确", - audit_opinion: "合同中违约责任的具体计算方式和赔偿标准需要进一步明确", - deduction_score: -3, - status: "rejected", - created_at: "2024-01-13 09:15:00", - is_vote: false, - voter_count: 2, - proposer_name: "王五", - current_user_is_proposer: false - }, - { - id: 11, - evaluation_point_id: 103, - document_id: documentId, - audit_point: "合同条款审查", - found_issue: "违约责任条款不明确", - audit_opinion: "合同中违约责任的具体计算方式和赔偿标准需要进一步明确", - deduction_score: -3, - status: "rejected", - created_at: "2024-01-13 09:15:00", - is_vote: false, - voter_count: 2, - proposer_name: "王五", - current_user_is_proposer: false - }, - ]; - - // 模拟分页 - const total = mockOpinions.length; - const startIndex = (page - 1) * pageSize; - const endIndex = startIndex + pageSize; - const paginatedOpinions = mockOpinions.slice(startIndex, endIndex); - + body: JSON.stringify({ + user_id: realUserId, + document_id: documentId, // 如果后端需要document_id可以加上 + page, + page_size: pageSize + }) + }); + if (!response.ok) { + throw new Error('获取意见列表失败'); + } + const data = await response.json(); + + // 处理新的数据结构,支持分页 + const responseData = data.data || data; + const pagination = data.pagination; + + // 定义后端返回的数据项类型 + interface ProposalItem { + proposal_id: string | number; + evaluation_point_name: string; + proposed_score: number; + reason: string; + proposer: string; + votes?: Array<{ voter: string; vote_type: string }>; + agree_voters?: string[]; + disagree_voters?: string[]; + 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; + } + + // 适配后端返回结构,使用新字段 + const opinions: CrossCheckingOpinion[] = Array.isArray(responseData) ? responseData.map((item: ProposalItem) => ({ + proposal_id: item.proposal_id, + evaluation_point_name: item.evaluation_point_name, + proposed_score: item.proposed_score, + reason: item.reason, + proposer: item.proposer, + votes: item.votes || [], + agree_voters: item.agree_voters || [], + disagree_voters: item.disagree_voters || [], + 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 + })) : []; + return { data: { - opinions: paginatedOpinions, - total: total + opinions, + total: pagination?.total || opinions.length } }; } catch (error) { diff --git a/app/api/cross-checking/cross-files.ts b/app/api/cross-checking/cross-files.ts index 2afe72a..d05a71d 100644 --- a/app/api/cross-checking/cross-files.ts +++ b/app/api/cross-checking/cross-files.ts @@ -35,6 +35,32 @@ export interface CrossCheckingTask { documentIds: number[]; } +// 用户任务文档接口类型定义 +export interface UserTaskDocument { + document_id: number; + document_name: string; + document_type_id: number; + document_type_name: string; +} + +// 用户任务信息接口 +export interface UserTaskInfo { + task_id: number; + task_status: string; + documents: UserTaskDocument[]; +} + +// 用户任务API响应格式 +export interface UserTaskApiResponse { + data: UserTaskInfo[]; + pagination: { + page: number; + page_size: number; + total: number; + total_pages: number; + }; +} + // API响应格式 export interface ApiResponse { success: boolean; @@ -148,9 +174,46 @@ const mockTasks: CrossCheckingTask[] = [ */ export async function getCrossCheckingTasks(params: TaskListParams = {}): Promise> { try { - // TODO 这个需要对接获取交叉评查任务列表的接口 模拟API延迟 - await new Promise(resolve => setTimeout(resolve, 500)); + console.log('开始调用getCrossCheckingTasks,参数:', params); + // 调用用户任务API,获取当前用户参与的任务 + const userTasksResponse = await getUserTaskDocuments(1); // 暂时使用固定用户ID 1 + + console.log('getUserTaskDocuments响应:', userTasksResponse); + + if (!userTasksResponse.success || !userTasksResponse.data) { + console.error('获取用户任务失败:', userTasksResponse.error); + return { + success: false, + error: userTasksResponse.error || '获取用户任务失败' + }; + } + + // 将用户任务数据转换为CrossCheckingTask格式 + const userTasks = userTasksResponse.data; + const convertedTasks: CrossCheckingTask[] = userTasks.map((userTask: UserTaskInfo, index: number) => { + // 从用户任务中提取任务信息,如果没有对应信息则使用默认值 + const task: CrossCheckingTask = { + id: userTask.task_id, + sequence: index + 1, + taskName: `任务 ${userTask.task_id}`, // 用户任务API中没有任务名称,使用默认值 + startDate: new Date().toISOString().split('T')[0], // 使用当前日期作为默认值 + taskType: CrossCheckingTaskType.CITY, // 默认任务类型 + docType: CrossCheckingDocType.PENALTY, // 默认案卷类型 + evaluationRegion: '待定', // 默认评查地区 + progress: userTask.task_status === 'completed' ? 100 : + userTask.task_status === 'in_progress' ? 50 : 0, + status: userTask.task_status === 'completed' ? CrossCheckingTaskStatus.COMPLETED : + userTask.task_status === 'in_progress' ? CrossCheckingTaskStatus.IN_PROGRESS : + CrossCheckingTaskStatus.PENDING, + score: userTask.task_status === 'completed' ? 85 : 0, // 默认分数 + operation: userTask.task_status === 'completed' ? '查看结果' : + userTask.task_status === 'in_progress' ? '进行中' : '去评查', + documentIds: userTask.documents.map((doc: UserTaskDocument) => doc.document_id) + }; + return task; + }); + const { page = 1, pageSize = 10, @@ -163,7 +226,7 @@ export async function getCrossCheckingTasks(params: TaskListParams = {}): Promis } = params; // 筛选数据 - let filteredTasks = [...mockTasks]; + let filteredTasks = [...convertedTasks]; // 按任务类型筛选 if (taskType && taskType !== 'all') { @@ -314,14 +377,46 @@ export async function getCrossCheckingTaskDetail( total: number; }>> { try { - const task = mockTasks.find(t => t.id === taskId); - if (!task) { + // 从用户任务API中获取任务信息 + const userTasksResponse = await getUserTaskDocuments(1); // 暂时使用固定用户ID 1 + + if (!userTasksResponse.success || !userTasksResponse.data) { + console.error('获取用户任务失败:', userTasksResponse.error); + return { + success: false, + error: userTasksResponse.error || '获取用户任务失败' + }; + } + + // 查找指定的任务 + const userTask = userTasksResponse.data.find(t => t.task_id === taskId); + if (!userTask) { return { success: false, error: '任务不存在' }; } + // 将用户任务转换为CrossCheckingTask格式 + const task: CrossCheckingTask = { + id: userTask.task_id, + sequence: 1, // 暂时使用默认值 + taskName: `任务 ${userTask.task_id}`, // 用户任务API中没有任务名称,使用默认值 + startDate: new Date().toISOString().split('T')[0], // 使用当前日期作为默认值 + taskType: CrossCheckingTaskType.CITY, // 默认任务类型 + docType: CrossCheckingDocType.PENALTY, // 默认案卷类型 + evaluationRegion: '待定', // 默认评查地区 + progress: userTask.task_status === 'completed' ? 100 : + userTask.task_status === 'in_progress' ? 50 : 0, + status: userTask.task_status === 'completed' ? CrossCheckingTaskStatus.COMPLETED : + userTask.task_status === 'in_progress' ? CrossCheckingTaskStatus.IN_PROGRESS : + CrossCheckingTaskStatus.PENDING, + score: userTask.task_status === 'completed' ? 85 : 0, // 默认分数 + operation: userTask.task_status === 'completed' ? '查看结果' : + userTask.task_status === 'in_progress' ? '进行中' : '去评查', + documentIds: userTask.documents.map(doc => doc.document_id) + }; + let files: import('../evaluation_points/rules-files').ReviewFileUI[] = []; let total = 0; @@ -376,10 +471,24 @@ export async function getCrossCheckingStats(): Promise> { try { - const totalTasks = mockTasks.length; - const pendingTasks = mockTasks.filter(t => t.status === CrossCheckingTaskStatus.PENDING).length; - const inProgressTasks = mockTasks.filter(t => t.status === CrossCheckingTaskStatus.IN_PROGRESS).length; - const completedTasks = mockTasks.filter(t => t.status === CrossCheckingTaskStatus.COMPLETED).length; + console.log('开始调用getCrossCheckingStats'); + + // 获取用户任务数据来计算统计 + const userTasksResponse = await getUserTaskDocuments(1); // 暂时使用固定用户ID 1 + + if (!userTasksResponse.success || !userTasksResponse.data) { + console.error('获取用户任务失败:', userTasksResponse.error); + return { + success: false, + error: userTasksResponse.error || '获取用户任务失败' + }; + } + + const userTasks = userTasksResponse.data; + const totalTasks = userTasks.length; + const pendingTasks = userTasks.filter(t => t.task_status === 'pending').length; + const inProgressTasks = userTasks.filter(t => t.task_status === 'in_progress').length; + const completedTasks = userTasks.filter(t => t.task_status === 'completed').length; return { success: true, @@ -398,3 +507,103 @@ export async function getCrossCheckingStats(): Promise> { + try { + console.log('开始调用用户任务API,用户ID:', userId); + + // 导入API配置以显示当前使用的baseUrl + const { API_BASE_URL } = await import('../../config/api-config'); + console.log('当前API基础URL:', API_BASE_URL); + + // 调用真实的API接口 + console.log('调用API路径:', '/admin/cross_review/tasks/user_documents'); + console.log('完整API URL:', `${API_BASE_URL}/admin/cross_review/tasks/user_documents`); + console.log('请求参数:', { user_id: userId }); + + const response = await post( + '/admin/cross_review/tasks/user_documents', + { user_id: userId } + ); + + console.log('API响应:', response); + + // 如果API调用失败,尝试使用模拟数据作为回退 + if (response.error) { + console.warn('API调用失败,使用模拟数据作为回退'); + // 返回模拟数据 + const mockUserTasks: UserTaskInfo[] = [ + { + task_id: 1, + task_status: 'completed', + documents: [ + { document_id: 1, document_name: '测试文档1', document_type_id: 1, document_type_name: '行政处罚' }, + { document_id: 2, document_name: '测试文档2', document_type_id: 1, document_type_name: '行政处罚' } + ] + }, + { + task_id: 2, + task_status: 'in_progress', + documents: [ + { document_id: 3, document_name: '测试文档3', document_type_id: 2, document_type_name: '行政许可' } + ] + } + ]; + + return { + success: true, + data: mockUserTasks + }; + } + + if (response.error) { + console.error('获取用户任务及文档失败:', response.error); + return { + success: false, + error: response.error + }; + } + + // 确保返回的数据是数组格式 + let userTasks: UserTaskInfo[] = []; + + if (response.data) { + // 检查响应数据的结构 + console.log('响应数据结构:', response.data); + + // 根据实际API响应结构,数据在response.data.data中 + if (response.data.data && Array.isArray(response.data.data)) { + userTasks = response.data.data; + } else if (Array.isArray(response.data)) { + // 备用方案:如果数据直接在response.data中 + userTasks = response.data; + } else { + console.warn('响应数据格式不正确:', response.data); + userTasks = []; + } + } + + console.log('解析后的用户任务数据:', userTasks); + + return { + success: true, + data: userTasks + }; + } catch (error) { + console.error('获取用户任务及文档失败:', error); + return { + success: false, + error: error instanceof Error ? error.message : '获取用户任务及文档失败' + }; + } +} diff --git a/app/components/cross-checking/DocumentListModal.tsx b/app/components/cross-checking/DocumentListModal.tsx index 3d5a9f3..06de638 100644 --- a/app/components/cross-checking/DocumentListModal.tsx +++ b/app/components/cross-checking/DocumentListModal.tsx @@ -247,7 +247,7 @@ export function DocumentListModal({ <>
- 共有 + {total || files.length} 个文档
@@ -275,7 +275,7 @@ export function DocumentListModal({ ) : (
共 {total} 条记录,每页 {pageSize} 条 - {total <= pageSize && " (无需分页)"} + {total <= pageSize && ""}
)} diff --git a/app/components/cross-checking/ReviewPointsList.tsx b/app/components/cross-checking/ReviewPointsList.tsx index ed5e8ed..101df75 100644 --- a/app/components/cross-checking/ReviewPointsList.tsx +++ b/app/components/cross-checking/ReviewPointsList.tsx @@ -2231,6 +2231,14 @@ export function ReviewPointsList({ return result; }; + // 在ReviewPointsList组件内部 + useEffect(() => { + if (isOpinionListModalOpen && selectedReviewPoint?.documentId) { + loadOpinionListData(1, opinionListPageSize); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isOpinionListModalOpen, selectedReviewPoint?.documentId]); + // 组件主渲染函数 return ( <> @@ -2492,37 +2500,37 @@ export function ReviewPointsList({ ( -
{record.audit_point}
+
{record.evaluation_point_name}
) }, { - title: "发现问题", - key: "found_issue", + title: "问题描述", + key: "problem_message", width: "20%", render: (_: unknown, record: CrossCheckingOpinion) => ( -
{record.found_issue}
+
{record.problem_message}
) }, { - title: "审查意见", - key: "audit_opinion", + title: "调整理由", + key: "reason", width: "25%", render: (_: unknown, record: CrossCheckingOpinion) => ( -
{record.audit_opinion}
+
{record.reason}
) }, { - title: "评分", - key: "deduction_score", + title: "调整分数", + key: "proposed_score", width: "8%", align: "center" as const, render: (_: unknown, record: CrossCheckingOpinion) => ( - = 0 ? 'text-green-600' : 'text-red-600'}`}> - {record.deduction_score > 0 ? '+' : ''}{record.deduction_score} + = 0 ? 'text-green-600' : 'text-red-600'}`}> + {record.proposed_score > 0 ? '+' : ''}{record.proposed_score} ) }, @@ -2531,74 +2539,80 @@ export function ReviewPointsList({ key: "voter_count", width: "8%", align: "center" as const, - render: (_: unknown, record: CrossCheckingOpinion) => ( - {record.voter_count} - ) + render: (_: unknown, record: CrossCheckingOpinion) => { + // 投票类型配置 + const voterGroups = [ + { + type: "agree", + voters: record.agree_voters, + color: "text-green-700", + bg: "bg-green-100", + border: "border border-green-200" + }, + { + type: "disagree", + voters: record.disagree_voters, + color: "text-red-700", + bg: "bg-red-100", + border: "border border-red-200" + }, + { + type: "pending", + voters: record.pending_voters, + color: "text-gray-700", + bg: "bg-gray-100", + border: "border border-gray-200" + } + ]; + + return ( +
+ {voterGroups.map((group) => ( + Array.isArray(group.voters) && group.voters.length > 0 && ( +
+ {group.voters.map((name, idx) => ( + + {name} + + ))} +
+ ) + ))} +
+ ); + } }, { title: "意见发起人", - key: "proposer_name", + key: "proposer", width: "10%", render: (_: unknown, record: CrossCheckingOpinion) => ( -
{record.proposer_name}
+
+ + {record.proposer} + +
) }, { title: "操作", key: "operation", - width: "14%", + width: "18%", align: "center" as const, render: (_: unknown, record: CrossCheckingOpinion) => { - const isPerforming = (action: string) => performingAction === `${record.id}-${action}`; - + const isPerforming = (action: string) => performingAction === `${record.proposal_id}-${action}`; return ( -
- {/* 根据is_vote字段显示不同按钮 */} - {!record.is_vote ? ( - <> - - - - ) : ( - - )} - - {/* 如果当前用户是意见发起人,显示撤销意见按钮 */} - {record.current_user_is_proposer && ( - - )} -
+ ); } } @@ -2626,8 +2640,225 @@ export function ReviewPointsList({ )} - ); -} \ No newline at end of file +} + +// 操作按钮区美化+弹窗确认组件 +function OpinionActions({ record, isPerforming, handleOpinionAction }: { + record: CrossCheckingOpinion; + isPerforming: (action: string) => boolean; + handleOpinionAction: (id: string | number, action: OpinionActionType) => void; +}) { + const canVote = record.can_vote !== false; + const [showWithdrawModal, setShowWithdrawModal] = useState(false); + const [withdrawType, setWithdrawType] = useState<'withdraw_vote' | 'withdraw_opinion' | null>(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) { + timer = setTimeout(() => { + setCountdown((c) => c - 1); + }, 1000); + } else if (countdown === 0) { + setCounting(false); + } + return () => clearTimeout(timer); + }, [showWithdrawModal, counting, countdown]); + + const handleWithdrawConfirm = () => { + if (withdrawType && countdown === 0) { + handleOpinionAction(record.proposal_id, withdrawType); + setShowWithdrawModal(false); + setWithdrawType(null); + setCountdown(3); + setCounting(false); + } + }; + const handleWithdrawCancel = () => { + setShowWithdrawModal(false); + setWithdrawType(null); + setCountdown(3); + setCounting(false); + }; + return ( +
+ + + {(!canVote || record.is_vote) && ( + + )} + {record.current_user_is_proposer && ( + + )} + {showWithdrawModal && ( + + + +
+ } + > +
+
确定要撤销此操作吗?
+
评查点:{record.evaluation_point_name || record.proposal_id}
+
+ + )} + + ); +} + +// 交叉评查记录类型定义 +export interface CrossCheckingRecord { + id: string; + status: 'pending' | 'in_progress' | 'completed'; + // 可以根据需要添加更多字段 +} + +// 打开结果弹窗的函数(需要根据实际需求实现) +const openResultModal = (recordId: string) => { + // 这里实现打开结果弹窗的逻辑 + console.log('打开结果弹窗:', recordId); +}; + +// 交叉评查记录操作按钮组件 +export function ActionButtons({ record }: { record: CrossCheckingRecord }) { + // 根据记录状态确定按钮类型 + const getButtonConfig = () => { + switch (record.status) { + case 'pending': + return { + text: '去评查', + bgColor: 'bg-blue-600', + hoverColor: 'hover:bg-blue-700', + icon: ( + + + + ) + }; + case 'in_progress': + return { + text: '进行中', + bgColor: 'bg-gray-500', + hoverColor: 'hover:bg-gray-600', + icon: ( + + + + ) + }; + case 'completed': + default: + return { + text: '查看结果', + bgColor: 'bg-green-600', + hoverColor: 'hover:bg-green-700', + icon: ( + + + + ) + }; + } + }; + + const buttonConfig = getButtonConfig(); + + // 处理按钮点击事件 + const handleAction = () => { + switch (record.status) { + case 'pending': + // 跳转到评查页面 + window.location.href = `/review/${record.id}`; + break; + case 'in_progress': + // 进行中状态不执行操作 + break; + case 'completed': + // 打开结果弹窗或页面 + openResultModal(record.id); + break; + default: + break; + } + }; + + return ( + + ); +} \ No newline at end of file diff --git a/app/config/api-config.ts b/app/config/api-config.ts index ab03b0a..d8db31c 100644 --- a/app/config/api-config.ts +++ b/app/config/api-config.ts @@ -32,9 +32,9 @@ interface ApiConfig { const configs: Record = { // 开发环境 development: { - // baseUrl: 'http://172.16.0.55:8008', + baseUrl: 'http://172.16.0.55:8008', // baseUrl: 'http://172.16.0.81:3000', - baseUrl: 'http://nas.7bm.co:3000', + // baseUrl: 'http://nas.7bm.co:3000', // documentUrl: 'http://172.16.0.81:9000/docauditai/', documentUrl: 'http://172.16.0.55:8008/docauditai/', uploadUrl: 'http://172.16.0.55:8008/admin/documents', diff --git a/app/routes/cross-checking._index.tsx b/app/routes/cross-checking._index.tsx index 50bff8d..7df30f2 100644 --- a/app/routes/cross-checking._index.tsx +++ b/app/routes/cross-checking._index.tsx @@ -653,8 +653,8 @@ export default function CrossCheckingIndex() { pageSize={pageSize} onChange={handlePageChange} onPageSizeChange={handlePageSizeChange} - showTotal={true} - showPageSizeChanger={true} + showTotal={false} + showPageSizeChanger={false} pageSizeOptions={[10, 20, 30, 50]} /> )} diff --git a/app/routes/cross-checking.upload.tsx b/app/routes/cross-checking.upload.tsx index ff7c69b..3467c4d 100644 --- a/app/routes/cross-checking.upload.tsx +++ b/app/routes/cross-checking.upload.tsx @@ -1,4 +1,4 @@ -import { useState, useRef } from "react"; +import { useState, useRef, useEffect } from "react"; import { type MetaFunction, type ActionFunctionArgs } from "@remix-run/node"; import { Form, useNavigation, useNavigate } from "@remix-run/react"; import { UploadArea, type UploadAreaRef } from "~/components/ui/UploadArea"; @@ -16,6 +16,10 @@ import { formatFileSize, batchUploadCrossCheckingFiles } from "~/api/cross-checking/cross-files-upload"; +import { + getOrganizationTree, + convertToTreeData +} from "~/api/user"; import React from "react"; // Added for React.useState export const meta: MetaFunction = () => { @@ -50,310 +54,15 @@ export interface TreeNode { children?: TreeNode[]; } -// 无限层级组织架构数据结构 -const MOCK_TREE: TreeNode[] = [ - { - label: "梅州市", - value: "梅州市", - children: [ - { - label: "梅州市烟草局", // 市级局 - value: "梅州市烟草局", - children: [ - { label: "李局长", value: "梅州市-梅州市烟草局-李局长" }, - { label: "王副局长", value: "梅州市-梅州市烟草局-王副局长" }, - { - label: "市场监管科", // 市级局下的科室 - value: "梅州市烟草局-市场监管科", - children: [ - { label: "张科长", value: "梅州市-梅州市烟草局-市场监管科-张科长" }, - { label: "陈主任", value: "梅州市-梅州市烟草局-市场监管科-陈主任" } - ] - }, - { - label: "法规科", - value: "梅州市烟草局-法规科", - children: [ - { label: "刘科长", value: "梅州市-梅州市烟草局-法规科-刘科长" }, - { label: "周专员", value: "梅州市-梅州市烟草局-法规科-周专员" } - ] - } - ] - }, - { - label: "梅江区", // 区级 - value: "梅江区", - children: [ - { - label: "梅江区烟草分局", // 区级分局 - value: "梅江区烟草分局", - children: [ - { label: "张分局长", value: "梅州市-梅江区-梅江区烟草分局-张分局长" }, - { label: "李副分局长", value: "梅州市-梅江区-梅江区烟草分局-李副分局长" }, - { - label: "执法大队", // 分局下的大队 - value: "梅江区烟草分局-执法大队", - children: [ - { label: "王队长", value: "梅州市-梅江区-梅江区烟草分局-执法大队-王队长" }, - { label: "陈副队长", value: "梅州市-梅江区-梅江区烟草分局-执法大队-陈副队长" }, - { - label: "第一中队", // 大队下的中队 - value: "梅江区烟草分局-执法大队-第一中队", - children: [ - { label: "赵中队长", value: "梅州市-梅江区-梅江区烟草分局-执法大队-第一中队-赵中队长" }, - { label: "孙执法员", value: "梅州市-梅江区-梅江区烟草分局-执法大队-第一中队-孙执法员" }, - { label: "钱执法员", value: "梅州市-梅江区-梅江区烟草分局-执法大队-第一中队-钱执法员" } - ] - }, - { - label: "第二中队", - value: "梅江区烟草分局-执法大队-第二中队", - children: [ - { label: "吴中队长", value: "梅州市-梅江区-梅江区烟草分局-执法大队-第二中队-吴中队长" }, - { label: "郑执法员", value: "梅州市-梅江区-梅江区烟草分局-执法大队-第二中队-郑执法员" } - ] - } - ] - }, - { - label: "办公室", - value: "梅江区烟草分局-办公室", - children: [ - { label: "林主任", value: "梅州市-梅江区-梅江区烟草分局-办公室-林主任" }, - { label: "黄秘书", value: "梅州市-梅江区-梅江区烟草分局-办公室-黄秘书" } - ] - } - ] - }, - { - label: "梅江区市场监管局", - value: "梅江区市场监管局", - children: [ - { label: "刘局长", value: "梅州市-梅江区-梅江区市场监管局-刘局长" }, - { label: "周副局长", value: "梅州市-梅江区-梅江区市场监管局-周副局长" }, - { - label: "执法监察科", - value: "梅江区市场监管局-执法监察科", - children: [ - { label: "谢科长", value: "梅州市-梅江区-梅江区市场监管局-执法监察科-谢科长" }, - { label: "何专员", value: "梅州市-梅江区-梅江区市场监管局-执法监察科-何专员" } - ] - } - ] - } - ] - }, - { - label: "梅县区", // 另一个区 - value: "梅县区", - children: [ - { - label: "梅县区烟草分局", - value: "梅县区烟草分局", - children: [ - { label: "黄分局长", value: "梅州市-梅县区-梅县区烟草分局-黄分局长" }, - { label: "林副分局长", value: "梅州市-梅县区-梅县区烟草分局-林副分局长" }, - { - label: "稽查队", - value: "梅县区烟草分局-稽查队", - children: [ - { label: "吴队长", value: "梅州市-梅县区-梅县区烟草分局-稽查队-吴队长" }, - { label: "郑稽查员", value: "梅州市-梅县区-梅县区烟草分局-稽查队-郑稽查员" }, - { label: "谢稽查员", value: "梅州市-梅县区-梅县区烟草分局-稽查队-谢稽查员" } - ] - } - ] - } - ] - }, - { - label: "丰顺县", // 县级 - value: "丰顺县", - children: [ - { - label: "丰顺县烟草分局", - value: "丰顺县烟草分局", - children: [ - { label: "曾分局长", value: "梅州市-丰顺县-丰顺县烟草分局-曾分局长" }, - { - label: "专卖管理所", - value: "丰顺县烟草分局-专卖管理所", - children: [ - { label: "邓所长", value: "梅州市-丰顺县-丰顺县烟草分局-专卖管理所-邓所长" }, - { label: "罗管理员", value: "梅州市-丰顺县-丰顺县烟草分局-专卖管理所-罗管理员" } - ] - } - ] - } - ] - } - ] - }, - { - label: "揭阳市", - value: "揭阳市", - children: [ - { - label: "揭阳市烟草局", // 市级局 - value: "揭阳市烟草局", - children: [ - { label: "苏局长", value: "揭阳市-揭阳市烟草局-苏局长" }, - { label: "叶副局长", value: "揭阳市-揭阳市烟草局-叶副局长" }, - { - label: "专卖监督管理处", - value: "揭阳市烟草局-专卖监督管理处", - children: [ - { label: "潘处长", value: "揭阳市-揭阳市烟草局-专卖监督管理处-潘处长" }, - { label: "方副处长", value: "揭阳市-揭阳市烟草局-专卖监督管理处-方副处长" } - ] - } - ] - }, - { - label: "榕城区", - value: "榕城区", - children: [ - { - label: "榕城区烟草分局", - value: "榕城区烟草分局", - children: [ - { label: "王分局长", value: "揭阳市-榕城区-榕城区烟草分局-王分局长" }, - { label: "李明华", value: "揭阳市-榕城区-榕城区烟草分局-李明华" }, - { label: "张丽萍", value: "揭阳市-榕城区-榕城区烟草分局-张丽萍" }, - { - label: "市场检查组", - value: "榕城区烟草分局-市场检查组", - children: [ - { label: "陈组长", value: "揭阳市-榕城区-榕城区烟草分局-市场检查组-陈组长" }, - { label: "林检查员", value: "揭阳市-榕城区-榕城区烟草分局-市场检查组-林检查员" } - ] - } - ] - }, - { - label: "榕城区质监局", - value: "榕城区质监局", - children: [ - { label: "陈国强", value: "揭阳市-榕城区-榕城区质监局-陈国强" }, - { label: "林小芳", value: "揭阳市-榕城区-榕城区质监局-林小芳" } - ] - } - ] - }, - { - label: "揭东区", - value: "揭东区", - children: [ - { - label: "揭东区烟草分局", - value: "揭东区烟草分局", - children: [ - { label: "黄建军", value: "揭阳市-揭东区-揭东区烟草分局-黄建军" }, - { label: "吴秀英", value: "揭阳市-揭东区-揭东区烟草分局-吴秀英" }, - { label: "刘志华", value: "揭阳市-揭东区-揭东区烟草分局-刘志华" } - ] - } - ] - }, - { - label: "惠来县", // 县级 - value: "惠来县", - children: [ - { - label: "惠来县烟草分局", - value: "惠来县烟草分局", - children: [ - { label: "杨分局长", value: "揭阳市-惠来县-惠来县烟草分局-杨分局长" }, - { - label: "案件审理室", - value: "惠来县烟草分局-案件审理室", - children: [ - { label: "蔡主任", value: "揭阳市-惠来县-惠来县烟草分局-案件审理室-蔡主任" }, - { label: "郭审理员", value: "揭阳市-惠来县-惠来县烟草分局-案件审理室-郭审理员" } - ] - } - ] - } - ] - } - ] - }, - { - label: "汕头市", - value: "汕头市", - children: [ - { - label: "汕头市烟草局", // 市级局 - value: "汕头市烟草局", - children: [ - { label: "何局长", value: "汕头市-汕头市烟草局-何局长" }, - { label: "许副局长", value: "汕头市-汕头市烟草局-许副局长" } - ] - }, - { - label: "龙湖区", - value: "龙湖区", - children: [ - { - label: "龙湖区烟草分局", - value: "龙湖区烟草分局", - children: [ - { label: "许志明", value: "汕头市-龙湖区-龙湖区烟草分局-许志明" }, - { label: "蔡丽娜", value: "汕头市-龙湖区-龙湖区烟草分局-蔡丽娜" }, - { label: "郭建华", value: "汕头市-龙湖区-龙湖区烟草分局-郭建华" }, - { label: "何美霞", value: "汕头市-龙湖区-龙湖区烟草分局-何美霞" } - ] - }, - { - label: "龙湖区工商局", - value: "龙湖区工商局", - children: [ - { label: "方国庆", value: "汕头市-龙湖区-龙湖区工商局-方国庆" }, - { label: "杨小红", value: "汕头市-龙湖区-龙湖区工商局-杨小红" } - ] - } - ] - }, - { - label: "金平区", - value: "金平区", - children: [ - { - label: "金平区烟草分局", - value: "金平区烟草分局", - children: [ - { label: "邓志强", value: "汕头市-金平区-金平区烟草分局-邓志强" }, - { label: "罗美玲", value: "汕头市-金平区-金平区烟草分局-罗美玲" } - ] - }, - { - label: "金平区市场监管局", - value: "金平区市场监管局", - children: [ - { label: "苏建国", value: "汕头市-金平区-金平区市场监管局-苏建国" }, - { label: "叶丽华", value: "汕头市-金平区-金平区市场监管局-叶丽华" }, - { label: "潘志明", value: "汕头市-金平区-金平区市场监管局-潘志明" } - ] - } - ] - }, - { - label: "南澳县", // 县级 - value: "南澳县", - children: [ - { - label: "南澳县烟草分局", - value: "南澳县烟草分局", - children: [ - { label: "陈分局长", value: "汕头市-南澳县-南澳县烟草分局-陈分局长" }, - { label: "林管理员", value: "汕头市-南澳县-南澳县烟草分局-林管理员" } - ] - } - ] - } - ] - } -]; +// 默认的空组织架构数据(作为备用) +const DEFAULT_TREE: TreeNode[] = []; + +// 用户选择状态管理 +interface UserSelectionState { + treeData: TreeNode[]; + loading: boolean; + error: string | null; +} function isAllChildrenChecked(node: TreeNode, checked: string[]): boolean { @@ -440,6 +149,11 @@ export default function CrossCheckingUpload() { }); // 步骤2状态 const [groupChecked, setGroupChecked] = useState([]); + const [userSelectionState, setUserSelectionState] = useState({ + treeData: DEFAULT_TREE, + loading: false, + error: null + }); // 上传配置状态 - 设置默认值 const [priority] = useState("normal"); @@ -696,6 +410,65 @@ export default function CrossCheckingUpload() { const navigate = useNavigate(); + // 加载组织架构数据 + useEffect(() => { + const loadOrganizationData = async () => { + // 只在步骤2且数据为空且未在加载时执行 + if (currentStep === 2 && userSelectionState.treeData.length === 0 && !userSelectionState.loading) { + setUserSelectionState(prev => ({ ...prev, loading: true, error: null })); + + try { + console.log('开始加载组织架构数据'); + const response = await getOrganizationTree(true); + + if (response.success && response.data) { + console.log('原始API数据:', response.data); + const treeData = convertToTreeData(response.data.organizations); + console.log('转换后的树形数据:', treeData); + + // 验证数据转换是否正确 + treeData.forEach(org => { + console.log(`组织: ${org.label} (${org.value})`); + if (org.children) { + org.children.forEach(child => { + if (child.isUser) { + console.log(` - 用户: ${child.label} (${child.value})`); + } else { + console.log(` - 子组织: ${child.label} (${child.value})`); + } + }); + } + }); + + setUserSelectionState({ + treeData, + loading: false, + error: null + }); + } else { + console.error('获取组织架构失败:', response.error); + setUserSelectionState({ + treeData: DEFAULT_TREE, + loading: false, + error: response.error || '获取组织架构失败' + }); + toastService.error('获取组织架构失败,请刷新页面重试'); + } + } catch (error) { + console.error('加载组织架构数据失败:', error); + setUserSelectionState({ + treeData: DEFAULT_TREE, + loading: false, + error: error instanceof Error ? error.message : '加载组织架构数据失败' + }); + toastService.error('加载组织架构数据失败,请刷新页面重试'); + } + } + }; + + loadOrganizationData(); + }, [currentStep]); // 只依赖 currentStep,避免无限循环 + return (
@@ -751,7 +524,10 @@ export default function CrossCheckingUpload() { @@ -771,14 +547,26 @@ export default function CrossCheckingUpload() {
- { - setGroupChecked(values); - }} - /> + {userSelectionState.loading ? ( +
+ + 正在加载组织架构... +
+ ) : userSelectionState.error ? ( +
+ + 加载失败: {userSelectionState.error} +
+ ) : ( + { + setGroupChecked(values); + }} + /> + )}
{/* 右侧已选择成员显示区域 */} @@ -788,13 +576,25 @@ export default function CrossCheckingUpload() { {groupChecked.length > 0 ? (
{groupChecked.map((member, index) => { - const parts = member.split('-'); - const name = parts[parts.length - 1]; - const org = parts.slice(0, -1).join(' - '); + // 处理用户选择值,支持新的API格式 + let displayName = member; + let displayOrg = ''; + + if (member.startsWith('user_')) { + // 用户选择,格式为 user_123 + displayName = `用户ID: ${member.replace('user_', '')}`; + displayOrg = '用户'; + } else { + // 组织选择,格式为 ou_id 或 ou_id-ou_id + const parts = member.split('-'); + displayName = parts[parts.length - 1]; + displayOrg = parts.slice(0, -1).join(' - ') || '组织'; + } + return (
-
{name}
-
{org}
+
{displayName}
+
{displayOrg}
); })} @@ -819,7 +619,10 @@ export default function CrossCheckingUpload() { @@ -989,7 +792,10 @@ export default function CrossCheckingUpload() {