diff --git a/app/api/cross-checking/cross-file-result.ts b/app/api/cross-checking/cross-file-result.ts index 1779e54..76b26d2 100644 --- a/app/api/cross-checking/cross-file-result.ts +++ b/app/api/cross-checking/cross-file-result.ts @@ -26,15 +26,14 @@ function extractApiData(responseData: unknown): T | null { export interface SubmitOpinionRequest { reviewPointResultId: string | number; documentId: string | number; - auditPoint: string; - foundIssue: string; + evaluationPointId: number; // 必须是数字ID auditOpinion: string; deductionScore: number; } /** * 提出意见的响应接口 - */ +*/ export interface SubmitOpinionResponse { success: boolean; message: string; @@ -117,20 +116,20 @@ export async function findIsProposer(taskId: string | number, userId: number | u */ export async function submitCrossCheckingOpinion( opinionData: SubmitOpinionRequest, - jwtToken?: string + jwtToken?: string, + userInfo?: { user_id: number } ): Promise> { try { // 获取JWT token const token = await safeGetJWT(jwtToken); const requestData = { - proposer_user_id: 1, - evaluation_result_id: opinionData.reviewPointResultId, - // document_id: opinionData.documentId, - // audit_point: opinionData.auditPoint, - // found_issue: opinionData.foundIssue, - proposed_score: opinionData.deductionScore, - reason: opinionData.auditOpinion + document_id: opinionData.documentId, + evaluation_point_id: Number(opinionData.evaluationPointId), // 强制转数字 + proposed_score: opinionData.deductionScore, + reason: opinionData.auditOpinion, + proposer_id: userInfo?.user_id || 1, + evaluation_result_id: opinionData.reviewPointResultId }; const response = await fetch(`${API_BASE_URL}/admin/cross_review/proposals`, { diff --git a/app/api/cross-checking/cross-files-upload.ts b/app/api/cross-checking/cross-files-upload.ts index 0b628bf..c2c7dcf 100644 --- a/app/api/cross-checking/cross-files-upload.ts +++ b/app/api/cross-checking/cross-files-upload.ts @@ -65,8 +65,14 @@ export interface CrossCheckingUploadedFile { /** * 将文件转换为二进制数据 */ -export async function uploadFileToBinary(file: File): Promise { +export async function uploadFileToBinary(file: File | Blob): Promise { return new Promise((resolve, reject) => { + // 只保留简单类型检查和调试 + if (!(file instanceof File) && !(file instanceof Blob)) { + reject(new Error(`参数必须是File或Blob对象,当前类型: ${typeof file}`)); + return; + } + const reader = new FileReader(); reader.onload = () => { if (reader.result instanceof ArrayBuffer) { @@ -206,75 +212,69 @@ export async function uploadCrossCheckingDocument( } /** - * 批量上传交叉评查文件 + * 批量上传并自动分配交叉评查任务(新接口适配) * @param files 文件列表 * @param typeId 文档类型ID * @param priority 优先级 * @param documentNumber 文档编号 - * @param remark 备注信息 + * @param remark 备注 * @param isTestDocument 是否为测试文档 - * @returns 上传结果列表 + * @param assignUserIds 需要分配的用户ID数组 + * @param taskName 任务名称 + * @param docType 文档类型(如 XZCF、XZXK) + * @param token JWT Token */ -export async function batchUploadCrossCheckingFiles( +export async function batchUploadAndAssignCrossCheckingFiles( files: CrossCheckingUploadedFile[], typeId: number, priority: string = 'normal', documentNumber: string = '', remark: string = '', - isTestDocument: boolean = false + isTestDocument: boolean = false, + assignUserIds: number[], + taskName: string, + docType: string, + token: string | null = null ): Promise<{ - successes: Array<{file: CrossCheckingUploadedFile; result: CrossCheckingFileUploadResponse}>; + successes: Array<{file: CrossCheckingUploadedFile; result: Record}>; failures: Array<{file: CrossCheckingUploadedFile; error: string}>; }> { - const successes: Array<{file: CrossCheckingUploadedFile; result: CrossCheckingFileUploadResponse}> = []; + const successes: Array<{file: CrossCheckingUploadedFile; result: Record}> = []; const failures: Array<{file: CrossCheckingUploadedFile; error: string}> = []; - - console.log('【交叉评查批量上传】开始批量上传文件,文件数量:', files.length); - + const uploadEndpoint = '/cross_review/documents/upload_and_assign'; + const uploadUrl = UPLOAD_URL + uploadEndpoint; for (const fileInfo of files) { try { - console.log('【交叉评查批量上传】上传文件:', fileInfo.name); - - // 转换文件为二进制格式 - const binaryData = await uploadFileToBinary(fileInfo.file); - - // 上传文件 - const result = await uploadCrossCheckingDocument( - binaryData, - fileInfo.file.name, - fileInfo.file.type, - typeId, - priority, - documentNumber, - remark, - isTestDocument, - null, // 交叉评查文件通常没有关联的文档ID - false - ); - - if (result.error) { - console.error('【交叉评查批量上传】文件上传失败:', fileInfo.name, result.error); - failures.push({ - file: fileInfo, - error: result.error - }); - } else if (result.data) { - console.log('【交叉评查批量上传】文件上传成功:', fileInfo.name); - successes.push({ - file: fileInfo, - result: result.data - }); + const formData = new FormData(); + formData.append('file', fileInfo.file, fileInfo.name); + const uploadInfo = { + type_id: typeof typeId === 'string' ? parseInt(typeId, 10) : typeId, + evaluation_level: priority, + document_number: documentNumber || null, + remark: remark || null, + is_test_document: isTestDocument, + task_name: taskName, + doc_type: typeof docType === 'string' ? docType.toUpperCase() : docType + }; + formData.append('upload_info', JSON.stringify(uploadInfo)); + formData.append('assign_user_ids', JSON.stringify(assignUserIds)); + const headers: HeadersInit = {}; + if (token) headers['Authorization'] = `Bearer ${token}`; + const response = await fetch(uploadUrl, { + method: 'POST', + headers, + body: formData + }); + const result = await response.json(); + if (result && result.success) { + successes.push({ file: fileInfo, result }); + } else { + failures.push({ file: fileInfo, error: result.error || '未知错误' }); } } catch (error) { - console.error('【交叉评查批量上传】处理文件时发生错误:', fileInfo.name, error); - failures.push({ - file: fileInfo, - error: error instanceof Error ? error.message : '上传失败' - }); + failures.push({ file: fileInfo, error: error instanceof Error ? error.message : '上传失败' }); } } - - console.log('【交叉评查批量上传】批量上传完成,成功:', successes.length, '失败:', failures.length); return { successes, failures }; } diff --git a/app/api/user/user-management.ts b/app/api/user/user-management.ts index 0958f90..450f132 100644 --- a/app/api/user/user-management.ts +++ b/app/api/user/user-management.ts @@ -172,29 +172,19 @@ export function convertToTreeData(organizations: OrganizationNode[]): Array<{ }>; }> { return organizations.map(org => { - const children: Array<{ - label: string; - value: string; - isUser?: boolean; - userInfo?: UserInfo; - }> = []; - + // 递归处理子组织 + const subOrganizations = org.children && org.children.length > 0 ? convertToTreeData(org.children) : []; // 添加该组织下的用户 - if (org.users && org.users.length > 0) { - children.push(...org.users.map(user => ({ - label: user.nick_name, - value: `user_${user.id}`, - isUser: true, - userInfo: user - }))); - } - - // 递归处理子组织,保持原有的层级结构 - if (org.children && org.children.length > 0) { - const subOrganizations = convertToTreeData(org.children); - children.push(...subOrganizations); - } - + const userChildren = (org.users && org.users.length > 0) + ? org.users.map(user => ({ + label: user.nick_name, + value: `user_${user.id}`, + isUser: true, + userInfo: user + })) + : []; + // 合并子组织和用户 + const children = [...subOrganizations, ...userChildren]; return { label: org.ou_name, value: org.ou_id, diff --git a/app/components/cross-checking/ReviewPointsList.tsx b/app/components/cross-checking/ReviewPointsList.tsx index 2e58f27..a8187e2 100644 --- a/app/components/cross-checking/ReviewPointsList.tsx +++ b/app/components/cross-checking/ReviewPointsList.tsx @@ -31,6 +31,7 @@ import { type OpinionActionType } from '../../api/cross-checking/cross-file-result'; import { useFetcher, useNavigate } from '@remix-run/react'; +import { API_BASE_URL } from '~/config/api-config'; // import '../../styles/components/TooltipStyles.css'; /** @@ -86,6 +87,7 @@ export interface ReviewPoint { id: string; documentId?: string; pointId?: string; + evaluationPointId?: string | number; // 新增,允许兜底 editAuditStatusId?: string | number; editAuditStatus: number; editAuditStatusMessage?: string; // 添加审核意见字段 @@ -439,6 +441,17 @@ export function ReviewPointsList({ const [evaluationResultIds, setEvaluationResultIds] = useState([]); // 评分提案的evaluation_result_id const fetcher = useFetcher(); + // 归一化 reviewPoints,确保每个点都有 id 字段 + const [normalizedReviewPoints, setNormalizedReviewPoints] = useState([]); + console.log('normalizedReviewPoints', normalizedReviewPoints); + useEffect(() => { + const norm = reviewPoints.map(point => ({ + ...point, + id: String(point.id || point.evaluationPointId || point.pointId || '') // 保证 id 为字符串且不为 undefined + })); + setNormalizedReviewPoints(norm); + }, [reviewPoints]); + // 在组件中使用scoringProposals(这里只是简单使用以避免linter警告) // 将来可以用于显示相关的评分提案信息 useEffect(() => { @@ -690,11 +703,6 @@ export function ReviewPointsList({ return; } - if (opinionForm.deductionScore <= 0) { - toastService.error('扣分必须大于0'); - return; - } - if (opinionForm.deductionScore > 100) { toastService.error('扣分不能大于100分'); return; @@ -705,26 +713,75 @@ export function ReviewPointsList({ return; } - setIsSubmittingOpinion(true); + // 新增:详细打印每个校验条件 + console.log('校验前 selectedReviewPoint:', selectedReviewPoint); + console.log('校验前 opinionForm:', opinionForm); + console.log('校验前 userInfo:', userInfo); + console.log('documentId:', selectedReviewPoint.documentId, 'isNaN:', isNaN(Number(selectedReviewPoint.documentId)), 'typeof:', typeof selectedReviewPoint.documentId); + console.log('pointId:', selectedReviewPoint.pointId, 'isNaN:', isNaN(Number(selectedReviewPoint.pointId)), 'typeof:', typeof selectedReviewPoint.pointId); + console.log('deductionScore:', opinionForm.deductionScore, 'typeof:', typeof opinionForm.deductionScore, 'isNaN:', isNaN(Number(opinionForm.deductionScore))); + console.log('auditOpinion:', opinionForm.auditOpinion, 'trim:', String(opinionForm.auditOpinion).trim(), 'typeof:', typeof opinionForm.auditOpinion); + console.log('user_id:', userInfo?.user_id, 'typeof:', typeof userInfo?.user_id); + // 更严谨的校验逻辑 + if ( + selectedReviewPoint.documentId === undefined || + selectedReviewPoint.pointId === undefined || + opinionForm.deductionScore === undefined || + opinionForm.auditOpinion === undefined || + userInfo?.user_id === undefined || + isNaN(Number(selectedReviewPoint.documentId)) || + isNaN(Number(selectedReviewPoint.pointId)) || + isNaN(Number(opinionForm.deductionScore)) || + !String(opinionForm.auditOpinion).trim() + ) { + toastService.error('请完整填写所有必填项'); + setIsSubmittingOpinion(false); + return; + } + + // 打印所有关键数据 + console.log('selectedReviewPoint:', selectedReviewPoint); + console.log('opinionForm:', opinionForm); + console.log('userInfo:', userInfo); + + // 组装后端要求的字段名和内容 + const data: Record = { + document_id: Number(selectedReviewPoint.documentId), + evaluation_point_id: Number(selectedReviewPoint.pointId), + proposed_score: Number(opinionForm.deductionScore), + reason: opinionForm.auditOpinion, + proposer_id: userInfo.user_id, + problem_message: opinionForm.foundIssue, + evaluation_result_id: Number(selectedReviewPoint.id), + }; + if (selectedReviewPoint.evaluationPointId) { + data.evaluation_result_id = Number(selectedReviewPoint.evaluationPointId); + } + // 打印最终请求体 + console.log('最终请求体:', data); + // 用原生 fetch + application/json 提交 try { - // 使用 fetcher 调用路由的 action - const formData = new FormData(); - formData.append("intent", "submitCrossCheckingOpinion"); - formData.append("reviewPointResultId", selectedReviewPoint.id); - formData.append("documentId", selectedReviewPoint.documentId || ''); - formData.append("auditPoint", opinionForm.auditPoint); - formData.append("foundIssue", opinionForm.foundIssue); - formData.append("auditOpinion", opinionForm.auditOpinion); - formData.append("deductionScore", opinionForm.deductionScore.toString()); - - fetcher.submit(formData, { method: "POST" }); - + const response = await fetch(`${API_BASE_URL.replace(/\/$/, '')}/admin/cross_review/proposals`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${userInfo.frontend_jwt}`, + }, + body: JSON.stringify(data) + }); + const result = await response.json(); + if (response.ok) { + toastService.success('意见提交成功'); + handleCloseOpinionModal(); + } else { + toastService.error(result.detail || '提交意见失败'); + } } catch (error) { console.error('提交意见失败:', error); toastService.error('提交意见失败,请稍后重试'); - setIsSubmittingOpinion(false); } + setIsSubmittingOpinion(false); }; /** @@ -2572,10 +2629,14 @@ export function ReviewPointsList({ { title: "调整理由", key: "reason", - width: "25%", - render: (_: unknown, record: CrossCheckingOpinion) => ( -
{record.reason}
- ) + width: "15%", + render: (_: unknown, record: CrossCheckingOpinion) => { + const reason = record.reason || ''; + const display = reason.length > 20 ? reason.slice(0, 20) + '...' : reason; + return ( + {display} + ); + } }, { title: "调整分数", @@ -2665,6 +2726,31 @@ export function ReviewPointsList({
{record.created_at}
) }, + { + title: "投票状态", + key: "opinion_status", + width: "10%", + render: (_: unknown, record: CrossCheckingOpinion) => { + let label = ''; + let color = ''; + switch (record.status) { + case 'approved': + label = '通过'; + color = 'text-green-600'; + break; + case 'rejected': + label = '不通过'; + color = 'text-red-600'; + break; + case 'pending': + default: + label = '投票中'; + color = 'text-yellow-600'; + break; + } + return {label}; + } + }, { title: "操作", key: "operation", diff --git a/app/routes/cross-checking._index.tsx b/app/routes/cross-checking._index.tsx index 89674d2..c16a1fc 100644 --- a/app/routes/cross-checking._index.tsx +++ b/app/routes/cross-checking._index.tsx @@ -71,6 +71,8 @@ export async function loader({ request }: LoaderFunctionArgs) { // 获取用户会话信息 const { getUserSession } = await import("~/api/login/auth.server"); const { userInfo, frontendJWT } = await getUserSession(request); + + console.log('frontendJWT', frontendJWT); // 获取任务列表和统计数据,传递用户信息和JWT const [tasksResponse, statsResponse] = await Promise.all([ diff --git a/app/routes/cross-checking.upload.tsx b/app/routes/cross-checking.upload.tsx index 68cf172..c30d86e 100644 --- a/app/routes/cross-checking.upload.tsx +++ b/app/routes/cross-checking.upload.tsx @@ -14,13 +14,14 @@ import { type CrossCheckingUploadedFile, generateFileId, formatFileSize, - batchUploadCrossCheckingFiles + batchUploadAndAssignCrossCheckingFiles } from "~/api/cross-checking/cross-files-upload"; import { getOrganizationTree, convertToTreeData } from "~/api/user"; import React from "react"; // Added for React.useState +import { API_BASE_URL } from '~/config/api-config'; export const meta: MetaFunction = () => { return [ @@ -144,14 +145,15 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { * @param token JWT Token * @returns 创建结果 */ -async function createCrossReviewTask(taskData: { +export async function createCrossReviewTask(taskData: { documentIds: number[]; userIds: number[]; assignerId: number; taskName: string; + docType: string; }, token: string) { try { - const response = await fetch('/admin/crossreview/tasks/assign', { + const response = await fetch(`${API_BASE_URL}/admin/cross_review/tasks/assign`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -161,7 +163,8 @@ async function createCrossReviewTask(taskData: { document_ids: taskData.documentIds, user_ids: taskData.userIds, assigner_id: taskData.assignerId, - task_name: taskData.taskName + task_name: taskData.taskName, + doc_type: taskData.docType }) }); @@ -207,7 +210,7 @@ export default function CrossCheckingUpload() { type: '市局交叉评查', }); // 步骤2状态 - const [groupChecked, setGroupChecked] = useState([]); + const [groupChecked, setGroupChecked] = useState(userInfo?.user_id ? [`user_${userInfo.user_id}`] : []); const [userSelectionState, setUserSelectionState] = useState({ treeData: DEFAULT_TREE, loading: false, @@ -419,30 +422,9 @@ export default function CrossCheckingUpload() { setIsUploading(true); try { - // 第一步:上传文件 - console.log("开始批量上传文件:", filesToUpload.length, "个,案卷类型:", caseType); - - const uploadResult = await batchUploadCrossCheckingFiles( - filesToUpload.map(f => f.file), - caseType, - priority, - isTestDocument, - frontendJWT - ); + // 第一步:上传文件并自动分配任务(新接口) + console.log("开始批量上传文件并分配任务:", filesToUpload.length, "个,案卷类型:", caseType); - const { successes, failures } = uploadResult; - - 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以特定前缀开头或有特定格式) @@ -455,18 +437,31 @@ export default function CrossCheckingUpload() { } // 创建任务数据 - const taskData = { - documentIds, - userIds, - assignerId: userInfo?.user_id || 1, // 使用当前用户ID作为分配者 - taskName: taskInfo.name + const docTypeMap = { + [CaseType.ADMINISTRATIVE_PENALTY]: 'XZCF', + [CaseType.ADMINISTRATIVE_PERMIT]: 'XZXK' }; - console.log("创建任务数据:", taskData); - - // 调用创建任务接口 - await createCrossReviewTask(taskData, frontendJWT); - + const uploadResult = await batchUploadAndAssignCrossCheckingFiles( + filesToUpload, + CASE_TYPE_TO_TYPE_ID[caseType], + priority, + documentNumber, + remark, + isTestDocument, + userIds, + taskInfo.name, + docTypeMap[caseType] || 'XZCF', + frontendJWT + ); + + const { successes, failures } = uploadResult; + + if (failures.length > 0) { + toastService.error(`文件上传或任务分配失败:${failures[0].error}`); + return; + } + // 任务创建成功 toastService.success("交叉评查任务创建成功!"); messageService.success( @@ -630,14 +625,28 @@ export default function CrossCheckingUpload() { } } }; - + loadOrganizationData(); }, [currentStep]); // 只依赖 currentStep,避免无限循环 + // 在 CrossCheckingUpload 组件内添加工具函数 + function findUserNameById(tree: TreeNode[], userId: string): string | null { + for (const node of tree) { + if (node.value === userId && (node as { isUser?: boolean }).isUser) { + return node.label; + } + if (node.children) { + const found = findUserNameById(node.children, userId); + if (found) return found; + } + } + return null; + } + return (
- {/* 步骤指示器 */ + {/* 步骤指示器 */}
{STEPS.map((step) => (
))}
- } {/* 步骤1:创建任务 */} {currentStep === 1 && ( @@ -741,16 +749,16 @@ export default function CrossCheckingUpload() { {groupChecked.length > 0 ? (
{groupChecked.map((member, index) => { - // 处理用户选择值,支持新的API格式 - let displayName = member; + let displayName: string = member; let displayOrg = ''; if (member.startsWith('user_')) { - // 用户选择,格式为 user_123 - displayName = `用户ID: ${member.replace('user_', '')}`; + // 查找真实用户名 + const userName = findUserNameById(userSelectionState.treeData, member) || member.replace('user_', ''); + displayName = userName; displayOrg = '用户'; } else { - // 组织选择,格式为 ou_id 或 ou_id-ou_id + // 组织 const parts = member.split('-'); displayName = parts[parts.length - 1]; displayOrg = parts.slice(0, -1).join(' - ') || '组织';