diff --git a/app/api/document-types/document-types.ts b/app/api/document-types/document-types.ts index 36606d9..7b46861 100644 --- a/app/api/document-types/document-types.ts +++ b/app/api/document-types/document-types.ts @@ -717,8 +717,7 @@ export async function updateDocumentType(id: string, documentType: DocumentTypeU name: documentType.name.trim(), description: documentType.description || '', evaluation_point_groups_ids: groupIds, - prompt_config: promptConfig, - code: documentType.code || null + prompt_config: promptConfig }; console.log('更新文档类型请求数据:', JSON.stringify(apiDocumentType, null, 2)); diff --git a/app/api/evaluation_points/reviews.ts b/app/api/evaluation_points/reviews.ts index ba135b0..20d48ea 100644 --- a/app/api/evaluation_points/reviews.ts +++ b/app/api/evaluation_points/reviews.ts @@ -1,4 +1,5 @@ -import { postgrestGet, type PostgrestParams } from "../postgrest-client"; +import { postgrestGet, type PostgrestParams, postgrestPut } from "../postgrest-client"; +import {getDocument} from "~/api/files/documents"; import dayjs from 'dayjs'; /** @@ -95,6 +96,13 @@ interface StatsData { * @returns 评查点结果列表和统计数据 */ export async function getReviewPoints(fileId: string) { + // 首先先获取这个文档的数据 + const documentData = await getDocument(fileId); + if (documentData.error) { + console.error("获取文档数据错误:", documentData.error); + return Response.json({ error: documentData.error }, { status: documentData.status || 500 }); + } + // 步骤1:根据fileId查询evaluation_results表 const evaluationResultsParams: PostgrestParams = { select: '*', @@ -195,16 +203,81 @@ export async function getReviewPoints(fileId: string) { data = result.evaluated_results.data || ''; } + // 提取页码数组 + let contentPage: number[] = []; + console.log('datacontent-------', data); + if (data && typeof data === 'object') { + try { + const dataObj = data as Record; + // 检查是否是预期的格式 {'立案报告表-完整性检查':'缺失部分内容'} + for (const key in dataObj) { + if (Object.prototype.hasOwnProperty.call(dataObj, key)) { + // 使用'-'分割获取前缀(如'立案报告表') + const prefix = key.split('-')[0]; + console.log('prefix-------', prefix); + // 检查document.data中的ocrResult是否存在这个key + if (documentData.data?.ocrResult && + typeof documentData.data.ocrResult === 'object') { + + // ocrResult可能有嵌套的ocr_result属性 + let ocrData: Record = documentData.data.ocrResult as Record; + + // 检查是否有嵌套的ocr_result对象 + if ('ocr_result' in ocrData && + ocrData.ocr_result && + typeof ocrData.ocr_result === 'object') { + ocrData = ocrData.ocr_result as Record; + } + + for (const ocrKey in ocrData) { + // 如果找到匹配的key + if (ocrKey === prefix && + ocrData[ocrKey] && + typeof ocrData[ocrKey] === 'object' && + 'pages' in ocrData[ocrKey]) { + + // 获取pages数组 + const pages = ocrData[ocrKey].pages; + if (Array.isArray(pages)) { + contentPage = pages; + } + break; + } + } + } + } + } + } catch (e) { + console.error('解析评查点data失败:', e); + contentPage = []; + } + } + return { id: result.id, title: message, pointName: point.name || '', groupName: group.name || '', - status: point.suggestion_message_type || '', + + status: point.suggestion_message_type || '', //评查点的评查结果状态 + // status: 'error', //评查点的评查结果状态 + content: data, + + contentPage: contentPage, + suggestion: point.suggestion_message || '', + // suggestion: '只是给建议的修改内容', + result: result.evaluated_results?.result, // 记录评查结果,用于统计 score: point.score || 0, + + postAction: point.post_action || '', + // postAction: 'manual', + + actionContent: point.action_config || '', + // actionContent: '用户提前在评查点输入过的修改内容', + legalBasis: point.references_laws || {} // legalBasis: { // name: '中华人民共和国食品安全法', @@ -271,5 +344,79 @@ export async function getReviewPoints(fileId: string) { }; // console.log("reviewInfo-------",JSON.stringify(reviewInfo,null,2)); - return { data: resultData, stats, reviewInfo }; + return { data: resultData, stats, reviewInfo, document: documentData.data }; +} + +/** + * 更新评查结果 + * @param resultId 评查结果ID + * @param result 评查结果 (true/false) + * @param message 评查意见 + * @returns 更新后的评查结果 + */ +export async function updateReviewResult(resultId: string, result: boolean, message: string): Promise<{ + data?: unknown; + error?: string; + status?: number; +}> { + try { + if (!resultId) { + return { error: '评查结果ID不能为空', status: 400 }; + } + + // 首先获取当前评查结果数据 + const currentResultResponse = await postgrestGet('evaluation_results', { + select: '*', + filter: { id: `eq.${resultId}` } + }); + + if (currentResultResponse.error) { + return { error: currentResultResponse.error, status: currentResultResponse.status }; + } + + const currentResultData = extractApiData(currentResultResponse.data); + + if (!currentResultData || !Array.isArray(currentResultData) || currentResultData.length === 0) { + return { error: '未找到评查结果数据', status: 404 }; + } + + const currentResult = currentResultData[0]; + const currentEvaluatedResults = currentResult.evaluated_results || {}; + + // 构建要更新的数据,保留原有字段,只更新result和message + const updatedEvaluatedResults = { + ...currentEvaluatedResults, + result: result, + message: message + }; + + const updatedData = { + evaluated_results: updatedEvaluatedResults + }; + + // 调用 API 更新数据 + const response = await postgrestPut( + 'evaluation_results', + updatedData, + { id: resultId } + ); + + 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 + }; + } } diff --git a/app/api/evaluation_points/rules-files.ts b/app/api/evaluation_points/rules-files.ts index 53a32ef..11982d9 100644 --- a/app/api/evaluation_points/rules-files.ts +++ b/app/api/evaluation_points/rules-files.ts @@ -50,6 +50,8 @@ export interface DocumentType { // 评查文件UI接口 export interface ReviewFileUI { id: string; + status: string; + path: string; fileName: string; fileCode: string; fileType: string; @@ -59,6 +61,8 @@ export interface ReviewFileUI { reviewStatus: string; reviewStatusCode: number; issueCount: number; + score?: number; + auditStatus: number | null; issues: Array<{ severity: 'info' | 'warning' | 'error' | 'critical'; message: string; @@ -77,6 +81,35 @@ export interface DocumentSearchParams { pageSize?: number; // 每页条数 } +// 添加评查结果和评查点类型定义 +// 评查结果类型 +interface EvaluationResult { + id: string | number; + document_id: string | number; + evaluation_point_id: string | number; + evaluated_results?: { + result?: boolean; + message?: string; + data?: string; + [key: string]: unknown; + }; + [key: string]: unknown; +} + +// 评查点类型 +interface EvaluationPoint { + id: string | number; + post_action?: string; + score?: number; + [key: string]: unknown; +} + +// 文档评查状态结果 +interface DocumentReviewResult { + status: number; + issueCount: number; +} + /** * 格式化日期 * @param dateString 日期字符串 @@ -120,7 +153,7 @@ function extractApiData(responseData: unknown): T | null { export function mapReviewStatusToUI(status: number | null): string { switch(status) { case 1: return 'pass'; - case 2: return 'warning'; + case -2: return 'warning'; case -1: return 'fail'; case 0: return 'pending'; default: return 'pending'; @@ -135,7 +168,7 @@ export function mapReviewStatusToUI(status: number | null): string { export function mapUIToReviewStatus(status: string): number { switch(status) { case 'pass': return 1; - case 'warning': return 2; + case 'warning': return -2; case 'fail': return -1; case 'pending': return 0; default: return 0; @@ -162,6 +195,8 @@ export function convertToReviewFileUI(document: Document, documentTypeName: stri const reviewFileUI: ReviewFileUI = { id: document.id.toString(), + status: document.status, + path: document.path, fileName: document.name, fileCode: document.document_number, fileType: documentTypeName, @@ -171,6 +206,8 @@ export function convertToReviewFileUI(document: Document, documentTypeName: stri reviewStatus: reviewStatus, reviewStatusCode: document.evaluations_status || 0, issueCount: 0, + score: 0, + auditStatus: document.audit_status, issues: [], createdBy: document.user_id?.toString() || '系统' }; @@ -300,13 +337,176 @@ export async function getReviewFiles(searchParams: DocumentSearchParams = {}): P documentTypes.forEach((type: DocumentTypeUI) => { typeNameMap[type.id] = type.name; }); + + // 获取评查文件的评查结果 + // 第一步:收集所有文档ID + const documentIds = extractedDocuments.map(doc => doc.id); - // 将文档数据转换为UI文件对象 - const reviewFiles = extractedDocuments.map(doc => { - const typeName = typeNameMap[doc.type_id] || '未知类型'; - return convertToReviewFileUI(doc, typeName); + // 第二步:查询所有文档的评查结果数据 + const evaluationResultParams: PostgrestParams = { + select: '*', + filter: { + 'document_id': `in.(${documentIds.join(',')})` + } + }; + + const evaluationResultsResponse = await postgrestGet('evaluation_results', evaluationResultParams); + let evaluationResults: EvaluationResult[] = []; + + if (!evaluationResultsResponse.error) { + evaluationResults = extractApiData(evaluationResultsResponse.data) || []; + } + + // 第三步:收集所有评查点ID + const evaluationPointIds = evaluationResults + .map(result => result.evaluation_point_id) + .filter(Boolean); + + // 第四步:获取评查点数据 + let evaluationPoints: EvaluationPoint[] = []; + if (evaluationPointIds.length > 0) { + const evaluationPointsParams: PostgrestParams = { + select: '*', + filter: { + 'id': `in.(${evaluationPointIds.join(',')})` + } + }; + + const evaluationPointsResponse = await postgrestGet('evaluation_points', evaluationPointsParams); + if (!evaluationPointsResponse.error) { + evaluationPoints = extractApiData(evaluationPointsResponse.data) || []; + } + } + + // 创建评查点ID到评查点数据的映射 + const pointsMap = new Map(); + evaluationPoints.forEach(point => { + pointsMap.set(point.id, point); }); + // 创建文档ID到评查结果列表的映射 + const documentResultsMap = new Map(); + evaluationResults.forEach(result => { + const docId = result.document_id; + if (!documentResultsMap.has(docId)) { + documentResultsMap.set(docId, []); + } + documentResultsMap.get(docId)!.push(result); + }); + + // 计算每个文档的评查状态和问题列表 + const documentStatusMap = new Map(); + // 存储每个文档的问题消息 + const documentIssuesMap = new Map>(); + // 存储每个文档的分数 + const documentScoreMap = new Map(); + + documentIds.forEach(docId => { + const results = documentResultsMap.get(docId) || []; + + // 1. 首先检查是否有需要人工审核的评查点 + let hasManualReviewPoint = false; + let hasFailResult = false; + let totalScore = 0; + let totalPoints = 0; + + // 存储该文档的问题消息 + const issuesList: Array<{severity: 'info' | 'warning' | 'error' | 'critical', message: string}> = []; + + for (const result of results) { + const evaluatedResults = result.evaluated_results || {}; + const resultValue = evaluatedResults.result; + const pointId = result.evaluation_point_id; + const point = pointsMap.get(pointId); + + // 检查是否需要人工审核 + if (resultValue === false && point && point.post_action === 'manual') { + hasManualReviewPoint = true; + } + + // 检查是否有不通过的结果 + if (resultValue === false) { + hasFailResult = true; + + // 收集问题消息 + if (evaluatedResults.message) { + issuesList.push({ + severity: 'error', + message: evaluatedResults.message as string + }); + } + } + + // 计算总分 + if (point) { + totalScore += point.score || 0; + totalPoints++; + } + } + + // 保存文档的问题列表 + documentIssuesMap.set(docId, issuesList); + + // 计算并保存文档的分数 + const calculatedScore = totalScore || 100; + documentScoreMap.set(docId, calculatedScore); + + // 根据优先级确定评查状态 + let status = 1; // 默认为通过 + + // 待人工确认优先级最高 + if (hasManualReviewPoint) { + status = 0; // 待人工确认 + } + // 警告次之 + else if (hasFailResult) { + status = -2; // 警告 + } + // 最后判断分数 + else { + // 如果没有评查点,默认为通过 + if (totalPoints > 0) { + // 通过分数线为80分 + status = totalScore >= 80 ? 1 : -1; // 通过或不通过 + } + } + + documentStatusMap.set(docId, { + status, + issueCount: results.filter(r => r.evaluated_results?.result === false).length + }); + }); + + // 将文档数据转换为UI文件对象,同时应用评查状态 + const reviewFiles = extractedDocuments.map(doc => { + const typeName = typeNameMap[doc.type_id] || '未知类型'; + const reviewResult = documentStatusMap.get(doc.id) || { status: doc.evaluations_status || 0, issueCount: 0 }; + const issues = documentIssuesMap.get(doc.id) || []; + const score = documentScoreMap.get(doc.id) || 100; // 获取计算后的分数,默认为100 + + // 如果文档的评查状态与计算结果不同,更新文档的评查状态 + if (doc.evaluations_status !== reviewResult.status) { + // 异步更新文档评查状态 + postgrestPut('documents', + { evaluations_status: reviewResult.status }, + { id: doc.id } + ).catch(err => console.error(`更新文档${doc.id}评查状态失败:`, err)); + } + + const reviewFile = convertToReviewFileUI(doc, typeName); + + // 覆盖文档的评查状态和问题计数 + reviewFile.reviewStatusCode = reviewResult.status; + reviewFile.reviewStatus = mapReviewStatusToUI(reviewResult.status); + reviewFile.issueCount = reviewResult.issueCount; + reviewFile.score = score; // 添加分数 + // 添加问题列表 + reviewFile.issues = issues; + + return reviewFile; + }); + + console.log('reviewFiles-----',reviewFiles); return { data: { files: reviewFiles, @@ -374,3 +574,39 @@ export async function updateReviewStatus(id: string, status: string): Promise<{ } } +/** + * 更新文件的审核状态 + * @param id 文件ID + * @param auditStatus 审核状态 + * @returns 更新结果 + */ +export async function updateDocumentAuditStatus(id: string, auditStatus: number): Promise<{ + success?: boolean; + error?: string; + status?: number; +}> { + try { + if (!id) { + return { error: '文件ID不能为空', status: 400 }; + } + + const response = await postgrestPut>( + 'documents', + { audit_status: auditStatus }, + { id: parseInt(id) } + ); + + if (response.error) { + return { error: response.error, status: response.status }; + } + + return { success: true }; + } catch (error) { + console.error('更新文件审核状态失败:', error); + return { + error: error instanceof Error ? error.message : '更新文件审核状态失败', + status: 500 + }; + } +} + diff --git a/app/api/files/documents.ts b/app/api/files/documents.ts index 2cc0fa1..99ca0e8 100644 --- a/app/api/files/documents.ts +++ b/app/api/files/documents.ts @@ -102,6 +102,7 @@ export interface DocumentUI { isTest: boolean; updatedAt?: string; pageCount?: number; + ocrResult?: unknown; } /** @@ -175,7 +176,8 @@ async function convertToUIDocument(doc: Document): Promise { path: doc.path, isTest: doc.is_test_document, updatedAt: formatDate(doc.updated_at), - pageCount: doc.ocr_result?.__meta?.page_count || 0 + pageCount: doc.ocr_result?.__meta?.page_count || 0, + ocrResult: doc.ocr_result }; } diff --git a/app/api/home/home.ts b/app/api/home/home.ts new file mode 100644 index 0000000..9eefad2 --- /dev/null +++ b/app/api/home/home.ts @@ -0,0 +1,249 @@ +import { postgrestGet, type PostgrestParams } from "../postgrest-client"; +import dayjs from 'dayjs'; + +/** + * 从不同格式的 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; +} + +/** + * 评估结果类型定义 + */ +interface EvaluationResult { + result: boolean; + rule_id?: string; + rule_name?: string; + description?: string; + [key: string]: unknown; +} + +/** + * 首页数据统计响应类型 + */ +interface HomeStatistics { + todayPendingFiles: number; + monthlyReviewedFiles: number; + monthlyReviewGrowth: { + value: number; + isUp: boolean; + }; + monthlyPassRate: number; + passRateGrowth: { + value: number; + isUp: boolean; + }; + issuesDetected: number; + issuesGrowth: { + value: number; + isUp: boolean; + }; +} + +/** + * 获取主页数据 + * @returns 主页数据 + */ +export async function getHomeData(): Promise { + try { + // 获取当前日期和时间相关值 + const startOfToday = dayjs().startOf('day').format('YYYY-MM-DD HH:mm:ss'); + const startOfThisMonth = dayjs().startOf('month').format('YYYY-MM-DD HH:mm:ss'); + const endOfThisMonth = dayjs().endOf('month').format('YYYY-MM-DD HH:mm:ss'); + const startOfLastMonth = dayjs().subtract(1, 'month').startOf('month').format('YYYY-MM-DD HH:mm:ss'); + const endOfLastMonth = dayjs().subtract(1, 'month').endOf('month').format('YYYY-MM-DD HH:mm:ss'); + + // 1. 今日待审核文件 - 获取今天的待审核文件数量 (audit_status = 0 或 2) + const todayPendingParams: PostgrestParams = { + select: 'count', + filter: { + or: `(audit_status.eq.0,audit_status.eq.2)`, + created_at: `gte.${startOfToday}` + } + }; + const todayPendingResponse = await postgrestGet('documents', todayPendingParams); + const todayPendingCount = extractApiData<{count: number}[]>(todayPendingResponse.data); + const todayPendingFiles = todayPendingCount?.[0]?.count || 0; + + // 2. 本月已审核文件 - 获取本月已审核文件数量 (audit_status != 0 且 != 2) + const thisMonthReviewedParams: PostgrestParams = { + select: 'count', + filter: { + and: `(audit_status.neq.0,audit_status.neq.2)`, + created_at: `gte.${startOfThisMonth}`, + created_at_lte: `lte.${endOfThisMonth}` + } + }; + const thisMonthReviewedResponse = await postgrestGet('documents', thisMonthReviewedParams); + const thisMonthReviewedCount = extractApiData<{count: number}[]>(thisMonthReviewedResponse.data); + const monthlyReviewedFiles = thisMonthReviewedCount?.[0]?.count || 0; + + // 上月已审核文件 + const lastMonthReviewedParams: PostgrestParams = { + select: 'count', + filter: { + and: `(audit_status.neq.0,audit_status.neq.2)`, + created_at: `gte.${startOfLastMonth}`, + created_at_lte: `lte.${endOfLastMonth}` + } + }; + const lastMonthReviewedResponse = await postgrestGet('documents', lastMonthReviewedParams); + const lastMonthReviewedCount = extractApiData<{count: number}[]>(lastMonthReviewedResponse.data); + const lastMonthReviewed = lastMonthReviewedCount?.[0]?.count || 0; + + // 计算同比增长 + let reviewGrowthValue = 0; + let reviewGrowthIsUp = true; + + if (lastMonthReviewed > 0) { + const growthRate = ((monthlyReviewedFiles - lastMonthReviewed) / lastMonthReviewed) * 100; + reviewGrowthValue = Math.abs(parseFloat(growthRate.toFixed(1))); + reviewGrowthIsUp = growthRate >= 0; + } + + // 3. 审核通过率 - 本月审核通过率 (已审核文件 / 总待审核文件 + 已审核文件) + const thisMonthTotalParams: PostgrestParams = { + select: 'count', + filter: { + created_at: `gte.${startOfThisMonth}`, + created_at_lte: `lte.${endOfThisMonth}` + } + }; + const thisMonthTotalResponse = await postgrestGet('documents', thisMonthTotalParams); + const thisMonthTotalCount = extractApiData<{count: number}[]>(thisMonthTotalResponse.data); + const thisMonthTotal = thisMonthTotalCount?.[0]?.count || 0; + + // 本月审核通过率 + const monthlyPassRate = thisMonthTotal > 0 + ? parseFloat(((monthlyReviewedFiles / thisMonthTotal) * 100).toFixed(1)) + : 0; + + // 上月审核通过率 + const lastMonthTotalParams: PostgrestParams = { + select: 'count', + filter: { + created_at: `gte.${startOfLastMonth}`, + created_at_lte: `lte.${endOfLastMonth}` + } + }; + const lastMonthTotalResponse = await postgrestGet('documents', lastMonthTotalParams); + const lastMonthTotalCount = extractApiData<{count: number}[]>(lastMonthTotalResponse.data); + const lastMonthTotal = lastMonthTotalCount?.[0]?.count || 0; + + const lastMonthPassRate = (lastMonthTotal > 0 && lastMonthReviewed > 0) + ? (lastMonthReviewed / lastMonthTotal) * 100 + : 0; + + // 计算通过率同比增长 + let passRateGrowthValue = 0; + let passRateGrowthIsUp = true; + + if (lastMonthPassRate > 0) { + const passRateGrowth = ((monthlyPassRate - lastMonthPassRate) / lastMonthPassRate) * 100; + passRateGrowthValue = Math.abs(parseFloat(passRateGrowth.toFixed(1))); + passRateGrowthIsUp = passRateGrowth >= 0; + } + + // 4. 检查出的问题总数(从评估结果表中统计) + const thisMonthIssuesParams: PostgrestParams = { + select: 'evaluated_results', + filter: { + created_at: `gte.${startOfThisMonth}`, + created_at_lte: `lte.${endOfThisMonth}` + } + }; + const thisMonthIssuesResponse = await postgrestGet('evaluation_results', thisMonthIssuesParams); + const thisMonthIssuesData = extractApiData<{ evaluated_results: EvaluationResult[] }[]>(thisMonthIssuesResponse.data); + + // 累计本月问题数量 + let thisMonthIssuesCount = 0; + if (thisMonthIssuesData && thisMonthIssuesData.length > 0) { + thisMonthIssuesData.forEach(row => { + if (row.evaluated_results && Array.isArray(row.evaluated_results)) { + thisMonthIssuesCount += row.evaluated_results.filter((result: EvaluationResult) => + result && result.result === false + ).length; + } + }); + } + + // 上月问题数量 + const lastMonthIssuesParams: PostgrestParams = { + select: 'evaluated_results', + filter: { + created_at: `gte.${startOfLastMonth}`, + created_at_lte: `lte.${endOfLastMonth}` + } + }; + const lastMonthIssuesResponse = await postgrestGet('evaluation_results', lastMonthIssuesParams); + const lastMonthIssuesData = extractApiData<{ evaluated_results: EvaluationResult[] }[]>(lastMonthIssuesResponse.data); + + let lastMonthIssuesCount = 0; + if (lastMonthIssuesData && lastMonthIssuesData.length > 0) { + lastMonthIssuesData.forEach(row => { + if (row.evaluated_results && Array.isArray(row.evaluated_results)) { + lastMonthIssuesCount += row.evaluated_results.filter((result: EvaluationResult) => + result && result.result === false + ).length; + } + }); + } + + // 计算问题数量同比增长 + let issuesGrowthValue = 0; + let issuesGrowthIsUp = true; + + if (lastMonthIssuesCount > 0) { + const issuesGrowth = ((thisMonthIssuesCount - lastMonthIssuesCount) / lastMonthIssuesCount) * 100; + issuesGrowthValue = Math.abs(parseFloat(issuesGrowth.toFixed(1))); + issuesGrowthIsUp = issuesGrowth >= 0; + } + + // 返回统计结果 + return { + todayPendingFiles, + monthlyReviewedFiles, + monthlyReviewGrowth: { + value: reviewGrowthValue, + isUp: reviewGrowthIsUp + }, + monthlyPassRate, + passRateGrowth: { + value: passRateGrowthValue, + isUp: passRateGrowthIsUp + }, + issuesDetected: thisMonthIssuesCount, + issuesGrowth: { + value: issuesGrowthValue, + isUp: issuesGrowthIsUp + } + }; + } catch (error) { + console.error('获取首页数据失败:', error); + // 返回默认值以防止页面崩溃 + return { + todayPendingFiles: 0, + monthlyReviewedFiles: 0, + monthlyReviewGrowth: { value: 0, isUp: true }, + monthlyPassRate: 0, + passRateGrowth: { value: 0, isUp: true }, + issuesDetected: 0, + issuesGrowth: { value: 0, isUp: true } + }; + } +} + diff --git a/app/components/layout/Sidebar.tsx b/app/components/layout/Sidebar.tsx index 82099f3..bef355d 100644 --- a/app/components/layout/Sidebar.tsx +++ b/app/components/layout/Sidebar.tsx @@ -37,24 +37,12 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) { path: '/files/upload', icon: 'ri-upload-cloud-line' }, - // { - // id: 'file-list', - // title: '文件列表', - // path: '/files', - // icon: 'ri-file-list-3-line' - // }, { id:'documents', title:'文档列表', path:'/documents', icon:'ri-file-list-3-line' } - // { - // id:'documents-edit', - // title:'文档编辑', - // path:'/documents/edit', - // icon:'ri-file-edit-line' - // } ] }, { @@ -87,12 +75,12 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) { path: '/rules-new', icon: 'ri-add-circle-line' }, - { - id: 'review-detail', - title: '评查详情', - path: '/reviews', - icon: 'ri-file-chart-line' - } + // { + // id: 'review-detail', + // title: '评查详情', + // path: '/reviews', + // icon: 'ri-file-chart-line' + // } ] }, { diff --git a/app/components/reviews/FileInfo.tsx b/app/components/reviews/FileInfo.tsx index f1368de..ee7c5c4 100644 --- a/app/components/reviews/FileInfo.tsx +++ b/app/components/reviews/FileInfo.tsx @@ -34,7 +34,7 @@ export function FileInfo({ fileInfo, onConfirmResults }: FileInfoProps) { {fileInfo.fileSize && ( - {fileInfo.fileSize} | {fileInfo.fileFormat} | {fileInfo.pageCount}页 + | {fileInfo.fileSize} | {fileInfo.fileFormat} | {fileInfo.pageCount}页 )} {fileInfo.uploadTime && ( diff --git a/app/components/reviews/FilePreview.tsx b/app/components/reviews/FilePreview.tsx index 1b89c64..63656f4 100644 --- a/app/components/reviews/FilePreview.tsx +++ b/app/components/reviews/FilePreview.tsx @@ -3,6 +3,11 @@ * 显示文档内容和评查点高亮 */ import { useState, useEffect, useRef } from 'react'; +import { Document, Page, pdfjs } from 'react-pdf'; + +// 设置worker路径为public目录下的worker文件 +// 使用已经下载的兼容版本 (pdfjs-dist v2.12.313) +pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.js'; // 导入统一的ReviewPoint类型 import { type ReviewPoint } from './'; @@ -11,6 +16,12 @@ import { type ReviewPoint } from './'; interface FileContent { title: string; contractNumber: string; + path: string; + ocrResult?: { + __meta?: { + page_offset?: number; + }; + }; // 添加ocrResult属性 parties: { partyA: { name: string; @@ -35,12 +46,15 @@ interface FilePreviewProps { fileContent: FileContent; reviewPoints: ReviewPoint[]; activeReviewPointId: string | null; + targetPage?: number; // 新增目标页码参数 } -export function FilePreview({ fileContent, reviewPoints, activeReviewPointId }: FilePreviewProps) { +export function FilePreview({ fileContent, reviewPoints, activeReviewPointId, targetPage }: FilePreviewProps) { const [zoomLevel, setZoomLevel] = useState(100); const [highlightsVisible, setHighlightsVisible] = useState(true); const contentRef = useRef(null); + const [numPages, setNumPages] = useState(null); + const [loadError, setLoadError] = useState(null); // 放大文档 const handleZoomIn = () => { @@ -77,6 +91,28 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointId }: } }, [activeReviewPointId]); + // 处理页面跳转 + useEffect(() => { + if (targetPage && numPages && targetPage <= numPages) { + let newTargetPage = targetPage; + try { + // 安全地访问ocrResult + if (fileContent.ocrResult && fileContent.ocrResult.__meta && fileContent.ocrResult.__meta.page_offset) { + // 可以根据需要使用page_offset调整目标页面 + newTargetPage = targetPage + fileContent.ocrResult.__meta.page_offset; + } + } catch (error) { + console.error("访问ocrResult时出错:", error); + } + + const pageElement = document.getElementById(`page-${newTargetPage}`); + if (pageElement) { + console.log(`跳转到第${newTargetPage}页`); + pageElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + } + }, [targetPage, numPages, fileContent]); + // 获取评查点对应的样式类 const getHighlightClass = (status: string) => { switch (status) { @@ -91,69 +127,114 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointId }: } }; + // PDF文档加载成功回调函数 + function onDocumentLoadSuccess({ numPages }: { numPages: number }) { + setNumPages(numPages); + console.log("PDF加载成功,页数:", numPages); + } + + /** + * 渲染PDF文档的所有页面 + * + * 功能描述: + * 1. 生成PDF所有页面的渲染数组,每个页面包含页码标识和实际页面内容 + * 2. 处理页面缩放,通过CSS transform实现页面大小调整 + * 3. 在每个页面上标记对应的评查点高亮区域 + * 4. 处理评查点的激活状态,显示特殊的高亮效果 + * + * @returns {JSX.Element[] | null} 返回所有页面组件的数组,如果没有页数信息则返回null + */ + const renderAllPages = () => { + // 如果还没有获取到PDF总页数,返回null + if (!numPages) return null; + + // 用于存储所有页面组件的数组 + const pages = []; + + // 遍历每一页,生成对应的页面组件 + for (let i = 1; i <= numPages; i++) { + // 查找该页面上的评查点,基于position.section匹配页面ID + const pageReviewPoints = reviewPoints.filter(point => + point.position && point.position.section === `page-${i}` + ); + // console.log("pageReviewPoints-------",pageReviewPoints); + + // 为每一页创建组件 + pages.push( +
+ {/* 页码标识,显示在页面上方 */} +
第 {i} 页
+ + {/* 页面容器,应用缩放变换,设置相对定位用于放置评查点高亮 */} +
+ {/* 渲染PDF页面组件 */} + + + {/* 渲染评查点高亮区域 */} + {highlightsVisible && pageReviewPoints.map(point => { + // 判断当前评查点是否为激活状态(被选中) + const isActive = point.id === activeReviewPointId; + + return ( + // 评查点高亮区域 +
+ ); + })} +
+
+ ); + } + + // 返回所有页面组件数组 + return pages; + }; + // 渲染文档内容 const renderDocumentContent = () => { return ( -
-

{fileContent.title}

-

合同编号:{fileContent.contractNumber}

- -
-

甲方(供方):{fileContent.parties.partyA.name}

-

地址:{fileContent.parties.partyA.address}

-

法定代表人:{fileContent.parties.partyA.representative}

-

联系电话:{fileContent.parties.partyA.phone}

-

 

-

乙方(需方):{fileContent.parties.partyB.name}

-

地址:{fileContent.parties.partyB.address}

-

法定代表人:{fileContent.parties.partyB.representative}

-

联系电话:{fileContent.parties.partyB.phone}

-
- -

根据《中华人民共和国合同法》及有关法律法规的规定,经双方协商一致,签订本合同,共同遵守。

- - {fileContent.sections.map((section, sectionIndex) => ( -
-

{section.title}

- {renderSectionContent(section.content, section.title, sectionIndex)} -
- ))} -
+ { + console.error("PDF加载错误:", error); + setLoadError("PDF文档加载失败:" + (error.message || "未知错误")); + }} + className="flex flex-col items-center" + error={
PDF文档加载失败,请检查链接或网络连接。
} + noData={
无数据
} + loading={
PDF加载中...
} + > + {renderAllPages()} +
); }; - // 渲染章节内容,处理高亮 - const renderSectionContent = (content: string, sectionTitle: string, sectionIndex: number) => { - const lines = content.split('\n'); - - return lines.map((line, lineIndex) => { - // 查找该行对应的评查点 - const reviewPoint = reviewPoints.find(point => - point.position && - point.position.section === sectionTitle && - point.position.index === lineIndex - ); - - if (reviewPoint && highlightsVisible) { - // 如果有对应的评查点,添加高亮 - const isActive = reviewPoint.id === activeReviewPointId; - return ( -
-

{line}

-
- ); - } else { - // 没有评查点,正常显示 - return

{line}

; - } - }); - }; - return (
@@ -182,8 +263,14 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointId }:
-
- {renderDocumentContent()} +
+ {loadError ? ( +
+

{loadError}

+
+ ) : ( + renderDocumentContent() + )}
); diff --git a/app/components/reviews/ReviewPointsList.tsx b/app/components/reviews/ReviewPointsList.tsx index 7f49d87..7492927 100644 --- a/app/components/reviews/ReviewPointsList.tsx +++ b/app/components/reviews/ReviewPointsList.tsx @@ -34,6 +34,7 @@ export interface ReviewPoint { humanReviewNote?: string; humanReviewBy?: string; humanReviewTime?: string; + contentPage?: number[]; position?: { section: string; index: number; @@ -45,6 +46,8 @@ export interface ReviewPoint { articles?: Array; [key: string]: unknown; }; + postAction?: string; + actionContent?: string; } // 统计数据类型 @@ -60,8 +63,8 @@ interface ReviewPointsListProps { reviewPoints: ReviewPoint[]; statistics: Statistics; activeReviewPointId: string | null; - onReviewPointSelect: (id: string) => void; - onStatusChange: (id: string, status: string) => void; + onReviewPointSelect: (id: string, page?: number) => void; + onStatusChange: (id: string, status: string, message: string) => void; } export function ReviewPointsList({ @@ -73,24 +76,35 @@ export function ReviewPointsList({ }: ReviewPointsListProps) { // 状态管理 const [editingReviewPoint, setEditingReviewPoint] = useState(null); // 当前正在编辑的评查点ID - const [userInputText, setUserInputText] = useState(''); // 用户输入的审核意见文本 const [searchText, setSearchText] = useState(''); // 搜索文本 const [statusFilter, setStatusFilter] = useState(null); // 状态过滤 // eslint-disable-next-line no-unused-vars const [suggestionTexts, setSuggestionTexts] = useState>({}); // 存储每个评查点的建议文本 + // 添加重新审核意见的状态/ 用户输入的修改内容 / 用户提前写好的修改内容 + const [manualReviewNotes, setManualReviewNotes] = useState>({}); + // 初始化建议文本 useEffect(() => { // 将所有评查点的建议文本存储到状态中 const suggestions: Record = {}; + reviewPoints.forEach(point => { suggestions[point.id] = point.suggestion || ''; }); setSuggestionTexts(suggestions); + + // 使用函数式更新,不再需要外部 manualReviewNotes 变量 + setManualReviewNotes(prev => { + const notes = {...prev}; + reviewPoints.forEach(point => { + notes[point.id] = point.actionContent || ''; + }); + return notes; + }); }, [reviewPoints]); // 处理建议文本变更 - // eslint-disable-next-line no-unused-vars const handleSuggestionChange = (reviewPointId: string, text: string) => { setSuggestionTexts(prev => ({ ...prev, @@ -98,11 +112,35 @@ export function ReviewPointsList({ })); }; + /** + * 处理评查点审核操作 + * @param reviewPointResultId 评查点结果ID + * @param action 操作类型: 'approve' 通过 / 'reject' 不通过 + * @param message 用户输入的审核内容 + */ + const handleReviewAction = (reviewPointResultId: string, action: 'approve' | 'reject', message: string) => { + // 更新评查点状态 + onStatusChange(reviewPointResultId, action === 'approve' ? 'true' : 'false', message); + + // 将参数输出到控制台 + console.log('评查点审核通过不通过操作', { + id: reviewPointResultId, + action: action, + content: message, + status: action === 'approve' ? 'true' : 'false' + }); + + // 清除编辑状态 + setEditingReviewPoint(null); + + alert(`${action === 'approve' ? '通过' : '不通过'}了评查点 ${reviewPointResultId},审核内容: ${message}`); + }; + /** * 过滤评查点 * 根据搜索文本和状态过滤条件筛选评查点 */ - const filteredReviewPoints = reviewPoints.filter(point => { + const filteredReviewPoints = reviewPoints.filter(point => { // 匹配搜索文本 const matchesSearch = searchText === '' || point.title.toLowerCase().includes(searchText.toLowerCase()) || @@ -146,36 +184,6 @@ export function ReviewPointsList({ // onStatusChange(reviewPointId, 'success'); }; - /** - * 处理评查点审核操作 - * @param reviewPointId 评查点ID - * @param action 操作类型: 'approve' 通过 / 'reject' 不通过 - */ - const handleReviewAction = (reviewPointId: string, action: 'approve' | 'reject') => { - // 更新评查点状态 - onStatusChange(reviewPointId, action === 'approve' ? 'success' : 'error'); - - // 清除编辑状态 - setEditingReviewPoint(null); - setUserInputText(''); - - alert(`${action === 'approve' ? '通过' : '不通过'}了评查点 ${reviewPointId}`); - }; - - /** - * 显示评查点详情编辑界面 - * @param reviewPointId 评查点ID - */ - const handleEditReviewPoint = (reviewPointId: string) => { - setEditingReviewPoint(reviewPointId); - - // 获取评查点的建议内容作为初始值 - const reviewPoint = reviewPoints.find(point => point.id === reviewPointId); - if (reviewPoint) { - setUserInputText(reviewPoint.suggestion || ''); - } - }; - /** * 渲染评查统计信息 * 显示总计、通过、警告、错误数量 @@ -215,7 +223,7 @@ export function ReviewPointsList({ {/* 总计数量 */}
- 总计
{/* 通过数量 */}
- 通过
{/* 警告数量 */}
- 警告
{/* 错误数量 */}
- 错误
@@ -374,9 +382,9 @@ export function ReviewPointsList({ * @returns 人工审核标记组件 */ const renderHumanReviewBadge = (reviewPoint: ReviewPoint) => { - if (reviewPoint.needsHumanReview) { + if (reviewPoint.postAction === 'manual') { return ( - + 需人工 ); @@ -411,6 +419,14 @@ export function ReviewPointsList({ * @returns 评查点内容组件 */ const renderReviewPointContent = (reviewPoint: ReviewPoint) => { + + const handleManualReviewNotesChange = (reviewPointId: string, text: string) => { + setManualReviewNotes(prev => ({ + ...prev, + [reviewPointId]: text + })); + }; + // 如果当前评查点不处于编辑状态,只显示简单信息 if (editingReviewPoint !== reviewPoint.id) { // 根据result和status决定渲染哪种样式 @@ -423,19 +439,77 @@ export function ReviewPointsList({

已处理

{reviewPoint.suggestion && (
-

{reviewPoint.suggestion}

+

{reviewPoint.suggestion}

)} ); } + + // 处理 result=true 且 postAction=manual 的情况 + if (reviewPoint.postAction === 'manual') { + const handleReReview = (reviewPointId: string, status: string) => { + const note = manualReviewNotes[reviewPointId] || ''; + if (!note.trim()) { + alert('请输入审核意见'); + return; + } + // 在实际应用中,这里应该调用API提交审核意见 + onStatusChange(reviewPointId, status, note); + alert(`提交重新审核意见: ${note}`); + // 可以添加提交成功后的状态更新等操作 + }; + + const handleNoteChange = (reviewPointId: string, text: string) => { + setManualReviewNotes(prev => ({ + ...prev, + [reviewPointId]: text + })); + }; + + return ( +
+ {reviewPoint.suggestion && ( +
+
+ +

{reviewPoint.suggestion}

+
+
+ )} + + + {/* 额外的文本输入框区域 */} +
+ +
+ +
+ +
+
+ ); + } + return null; } // 非通过状态,显示内容和修改建议 const isErrorStatus = reviewPoint.result === false && reviewPoint.status === 'error'; + return (
{reviewPoint.content !== null && ( @@ -443,8 +517,18 @@ export function ReviewPointsList({ (typeof reviewPoint.content === 'object' && Object.keys(reviewPoint.content).length > 0) ) && ( <> + {/* 建议内容显示区域 */} + {reviewPoint.suggestion && ( +
+
+ +

{reviewPoint.suggestion}

+
+
+ )} + {/* 内容显示区域 */} -
+
{/* 移除顶部的"当前值"标题,在每个内容项中显示 */} {typeof reviewPoint.content === 'object' && reviewPoint.content !== null ? ( // 当 content 是对象时的渲染方式 @@ -461,7 +545,7 @@ export function ReviewPointsList({ {value ? '' : '缺失'}
-

{value || ' '}

+

{value || (value === '' ? 占位符 : '')}

))}
@@ -475,7 +559,7 @@ export function ReviewPointsList({ {isErrorStatus ? '不符合规范' : '需优化'}
-

{reviewPoint.content || '(内容为空)'}

+

{reviewPoint.content || '(内容为空)'}

)} @@ -484,22 +568,22 @@ export function ReviewPointsList({ {reviewPoint.legalBasis && (typeof reviewPoint.legalBasis === 'object') && ( (reviewPoint.legalBasis.name || reviewPoint.legalBasis.content || (reviewPoint.legalBasis.articles && Array.isArray(reviewPoint.legalBasis.articles) && reviewPoint.legalBasis.articles.length > 0)) && ( -
+
法律依据
{reviewPoint.legalBasis.name && ( -

{reviewPoint.legalBasis.name}

+

{reviewPoint.legalBasis.name}

)} {reviewPoint.legalBasis.content && ( -

条款内容:{reviewPoint.legalBasis.content}

+

条款内容:{reviewPoint.legalBasis.content}

)} {reviewPoint.legalBasis.articles && Array.isArray(reviewPoint.legalBasis.articles) && reviewPoint.legalBasis.articles.length > 0 && (

相关条款:

-
    +
      {reviewPoint.legalBasis.articles.map((item, index) => ( -
    • +
    • {typeof item === 'string' ? item : typeof item === 'object' && item !== null ? (item.name ? `${item.name}: ${item.content || ''}` : @@ -515,22 +599,25 @@ export function ReviewPointsList({ )} {/* 建议修改区域 */} -
      -
      - 建议修改为: - {/* 符合规范 */} + {/* {(reviewPoint.postAction !== 'none') && ( */} +
      +
      + {reviewPoint.postAction === 'manual' ? "审核意见:" : "建议修改为:"} + {/* 符合规范 */} +
      +
      @@ -664,9 +765,22 @@ export function ReviewPointsList({ ); }; + // 处理评查点点击事件 + const handleReviewPointClick = (id: string) => { + // 找到被点击的评查点 + const reviewPoint = reviewPoints.find(point => point.id === id); + + // 如果评查点存在并且有contentPage数组,传递第一个页码 + if (reviewPoint && reviewPoint.contentPage && reviewPoint.contentPage.length > 0) { + onReviewPointSelect(id, reviewPoint.contentPage[0]); + } else { + onReviewPointSelect(id); + } + }; + // 组件主渲染函数 return ( -
      +
      {/* 面板头部 */}
      @@ -686,24 +800,24 @@ export function ReviewPointsList({
      -
      +
      {children}
      diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index 1750edb..d205725 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -8,6 +8,8 @@ import { FileTag, links as fileTagLinks } from "~/components/ui/FileTag"; import { Tag } from "~/components/ui/Tag"; import homeStyles from "~/styles/pages/home.css?url"; import { getDocuments, type DocumentUI } from "~/api/files/documents"; +import { useState, useEffect } from "react"; +import dayjs from 'dayjs'; // 文件处理状态选项 const fileProcessingStatusOptions = [ @@ -87,33 +89,69 @@ export async function loader() { export default function Index() { const { stats, recentFiles } = useLoaderData(); + const [currentDateTime, setCurrentDateTime] = useState<{ + date: string; + time: string; + }>({ + date: dayjs().format('YYYY年MM月DD日'), + time: dayjs().format('HH:mm:ss') + }); + + // 更新当前时间 + useEffect(() => { + // 使用dayjs格式化日期和时间 + const updateDateTime = () => { + const now = dayjs(); + setCurrentDateTime({ + date: now.format('YYYY年MM月DD日'), + time: now.format('HH:mm:ss') + }); + }; + + // 立即更新一次 + updateDateTime(); + + // 设置计时器,每秒更新一次 + const timerID = setInterval(updateDateTime, 1000); + + // 清理函数,组件卸载时清除计时器 + return () => clearInterval(timerID); + }, []); return (
      - {/* 页面标识 */} + {/* 页面头部 */} +
      +

      系统概览

      +
      + {currentDateTime.date} + | + {currentDateTime.time} +
      +
      {/* 统计卡片区域 */}
      - - + + - + {/* */} {/* */} diff --git a/app/routes/documents._index.tsx b/app/routes/documents._index.tsx index a458951..7f0ff45 100644 --- a/app/routes/documents._index.tsx +++ b/app/routes/documents._index.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { useSearchParams, Link, useLoaderData, useFetcher } from "@remix-run/react"; +import { useSearchParams, useLoaderData, useFetcher, useNavigate,Link } from "@remix-run/react"; import { type MetaFunction, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node"; import { Card } from "~/components/ui/Card"; import { Button } from "~/components/ui/Button"; @@ -11,6 +11,7 @@ import { FilterPanel, FilterSelect, SearchFilter, DateRangeFilter } from "~/comp import documentsIndexStyles from "~/styles/pages/documents_index.css?url"; import { getDocuments, deleteDocument, type DocumentUI } from "~/api/files/documents"; import { getDocumentTypes } from "~/api/document-types/document-types"; +import { updateDocumentAuditStatus } from "~/api/evaluation_points/rules-files"; // 导入样式 export function links() { @@ -119,11 +120,11 @@ export const action = async ({ request }: ActionFunctionArgs) => { // 审核状态筛选选项 const auditStatusOptions = [ // { value: "", label: "全部" }, + { value: "-2", label: "警告" }, { value: "-1", label: "不通过" }, { value: "0", label: "待审核" }, { value: "1", label: "通过" }, - { value: "2", label: "警告" }, - { value: "3", label: "审核中" }, + { value: "2", label: "审核中" }, ]; // 文件处理状态选项 @@ -148,10 +149,10 @@ const fileStatusOptions = [ // 审核状态选项及样式 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: "yellow", icon: "ri-alert-line" }, - "3": { label: "审核中", color: "purple", icon: "ri-search-line" }, + "2": { label: "审核中", color: "purple", icon: "ri-search-line" }, }; // 格式化文件大小 @@ -182,6 +183,7 @@ export default function DocumentsIndex() { const [selectedRowKeys, setSelectedRowKeys] = useState([]); const loaderData = useLoaderData(); const fetcher = useFetcher(); + const navigate = useNavigate(); // 从URL获取当前筛选条件 const search = searchParams.get("search") || ""; @@ -321,29 +323,41 @@ export default function DocumentsIndex() { }; // 下载文档 - const handleDownload = async (path: string, fileName: string) => { - console.log('handleDownload',path,fileName) + const handleDownload = async (path: string) => { try { - // 使用API获取授权的下载链接 - // const { data, error } = await getFileDownloadUrl(path); + const urlBefore = 'http://172.18.0.100:9000/docauditai/'; + const downloadUrl = `${urlBefore}${path}`; - // if (error || !data?.downloadUrl) { - // console.error('获取下载链接失败:', error); - // alert('获取下载链接失败: ' + (error || '未知错误')); - // return; - // } + // 使用fetch获取文件内容 + const response = await fetch(downloadUrl); + if (!response.ok) { + throw new Error(`下载失败: ${response.status} ${response.statusText}`); + } + + // 将响应转换为Blob + const blob = await response.blob(); + + // 创建Blob URL + const blobUrl = URL.createObjectURL(blob); // 创建一个隐藏的a标签并点击它 const a = document.createElement('a'); - // a.href = data.downloadUrl; - a.href = path; - a.download = fileName; // 设置下载的文件名 + a.style.display = 'none'; + a.href = blobUrl; + // 从路径中获取文件名 + const fileName = path.split('/').pop() || 'document'; + a.download = decodeURIComponent(fileName); document.body.appendChild(a); a.click(); - document.body.removeChild(a); - } catch (err) { - console.error('下载文件失败:', err); - alert('下载文件失败: ' + (err instanceof Error ? err.message : '未知错误')); + + // 清理 + setTimeout(() => { + document.body.removeChild(a); + URL.revokeObjectURL(blobUrl); + }, 100); + } catch (error) { + console.error('下载文件失败:', error); + alert(`下载文件失败: ${error instanceof Error ? error.message : '未知错误'}`); } }; @@ -412,6 +426,114 @@ export default function DocumentsIndex() { setSearchParams(params); }; + // 导出列表 + const handleExport = async () => { + // 如果没有文档,显示提示信息 + if (documents.length === 0) { + alert('当前页面没有文档可供导出'); + return; + } + + try { + // 创建一个ZIP文件 + const JSZip = await import('jszip').then(module => module.default); + const zip = new JSZip(); + + // 准备所有下载任务 + const downloadTasks = documents.map(async (doc: DocumentUI) => { + try { + if (!doc.path) { + console.warn(`文档 ${doc.name} 没有有效的路径`); + return; + } + + const urlBefore = 'http://172.18.0.100:9000/docauditai/'; + const downloadUrl = `${urlBefore}${doc.path}`; + + // 获取文件内容 + const response = await fetch(downloadUrl); + if (!response.ok) { + throw new Error(`下载失败: ${response.status} ${response.statusText}`); + } + + // 将响应转换为Blob + const blob = await response.blob(); + + // 从路径中获取文件名 + const fileName = doc.path.split('/').pop() || doc.name; + + // 添加到ZIP文件 + zip.file(decodeURIComponent(fileName), blob); + + return { success: true, name: fileName }; + } catch (error) { + console.error(`下载文件 ${doc.name} 失败:`, error); + return { success: false, name: doc.name, error }; + } + }); + + // 等待所有下载任务完成 + const results = await Promise.all(downloadTasks); + + // 计算成功和失败的数量 + const succeeded = results.filter(r => r && r.success).length; + const failed = results.filter(r => r && !r.success).length; + + if (succeeded === 0) { + alert('所有文件下载失败'); + return; + } + + // 生成ZIP文件 + const zipBlob = await zip.generateAsync({ type: 'blob' }); + + // 创建下载链接 + const url = URL.createObjectURL(zipBlob); + const a = document.createElement('a'); + a.href = url; + a.download = `文档导出_${new Date().toISOString().slice(0, 10)}.zip`; + document.body.appendChild(a); + a.click(); + + // 清理 + setTimeout(() => { + document.body.removeChild(a); + URL.revokeObjectURL(url); + }, 100); + + // 显示结果消息 + if (failed > 0) { + alert(`成功导出 ${succeeded} 个文件,${failed} 个文件失败`); + } else { + alert(`成功导出 ${succeeded} 个文件`); + } + } catch (error) { + console.error('导出文件失败:', error); + alert(`导出文件失败: ${error instanceof Error ? error.message : '未知错误'}`); + } + }; + + // 开始审核 + const handleReviewFileClick = async (fileId: number, auditStatus: number | null) => { + // 检查audit_status是否为0,如果是则更新为2 + if (auditStatus === 0) { + try { + const response = await updateDocumentAuditStatus(fileId.toString(), 2); + if (response.error) { + console.error('更新文件审核状态失败:', response.error); + alert('更新文件审核状态失败:' + (response.error || '未知错误')); + } + } catch (error) { + console.error('更新文件审核状态时出错:', error); + alert('更新文件审核状态时出错:' + (error instanceof Error ? error.message : '未知错误')); + } + } + + // 导航到评查详情页 + navigate(`/reviews?id=${fileId}`); + }; + + // 表格列定义 const columns = [ { @@ -517,7 +639,7 @@ export default function DocumentsIndex() { { title: "问题数量", key: "issues", - width:"5%", + width:"7%", render: (_: unknown, record: DocumentUI) => ( record.issues === null ? "-" : record.issues ) @@ -535,14 +657,20 @@ export default function DocumentsIndex() { render: (_: unknown, record: DocumentUI) => (
      {(record.auditStatus === 0 || record.auditStatus == null) ? ( - handleReviewFileClick(record.id, record.auditStatus)} + disabled={record.fileStatus !== 'Processed'} + className={`mr-1 ${ + record.fileStatus === 'Processed' + ? 'hover:underline hover:cursor-pointer text-primary' + : 'text-gray-400 cursor-not-allowed opacity-60' + }`} > - + 开始审核 - - ) : record.auditStatus === 3 ? ( + + ) : record.auditStatus === 3 ? ( + //record.auditStatus === 3 目前这个状态不存在,所以除了待审核(0)-开始审核,其他都是审核中(2)-查看 ) : ( @@ -569,7 +697,7 @@ export default function DocumentsIndex() { diff --git a/app/routes/documents.edit.tsx b/app/routes/documents.edit.tsx index 2324fbc..06a5309 100644 --- a/app/routes/documents.edit.tsx +++ b/app/routes/documents.edit.tsx @@ -198,9 +198,9 @@ export default function DocumentEdit() { // 在新窗口打开文档预览 const openPreview = () => { // 假设有一个预览URL的格式,比如 /preview?path=xxx - console.log('documentstest', document); - - const previewUrl = `/preview?path=${encodeURIComponent(document.path)}&name=${encodeURIComponent(document.name)}`; + // console.log('documentstest', document); + const urlBefore = 'http://172.18.0.100:9000/docauditai/' + const previewUrl = `${urlBefore}${document.path}`; window.open(previewUrl, '_blank'); }; diff --git a/app/routes/reviews.tsx b/app/routes/reviews.tsx index 08318bd..97d7df0 100644 --- a/app/routes/reviews.tsx +++ b/app/routes/reviews.tsx @@ -28,9 +28,8 @@ import { type MetaFunction, type LoaderFunctionArgs } from "@remix-run/node"; import { useState, useEffect } from "react"; import { useNavigate, useLoaderData } from "@remix-run/react"; -import {getDocument} from "~/api/files/documents"; import reviewsStyles from "~/styles/reviews.css?url"; -import { getReviewPoints } from "~/api/evaluation_points/reviews"; +import { getReviewPoints, updateReviewResult } from "~/api/evaluation_points/reviews"; // 导入评查详情页面组件 import { @@ -176,25 +175,28 @@ export async function loader({ request }: LoaderFunctionArgs) { return Response.json({ error: '评查ID不能为空' }, { status: 400 }); } - const documentData = await getDocument(id); - if (documentData.error) { - console.error("获取文档数据错误:", documentData.error); - return Response.json({ error: documentData.error }, { status: documentData.status || 500 }); - } + const reviewData = await getReviewPoints(id); - console.log("reviewData-------",JSON.stringify(reviewData.data,null,2)); - if (reviewData.error) { + // console.log("documentData-------",JSON.stringify(documentData.data,null,2)); + console.log("reviewData-------",JSON.stringify('data' in reviewData ? reviewData.data : '',null,2)); + if ('error' in reviewData && reviewData.error) { console.error("获取评查点数据错误:", reviewData.error); return Response.json({ error: reviewData.error }, { status: reviewData.status || 500 }); } - return Response.json({ - document: documentData.data, - reviewPoints: reviewData.data, - reviewInfo: reviewData.reviewInfo, - statistics: reviewData.stats - }); + // 确保reviewData有效且具有预期的属性 + if ('document' in reviewData && 'data' in reviewData && 'reviewInfo' in reviewData && 'stats' in reviewData) { + return Response.json({ + document: reviewData.document, + reviewPoints: reviewData.data, + reviewInfo: reviewData.reviewInfo, + statistics: reviewData.stats + }); + } else { + console.error("返回的评查数据格式不正确"); + return Response.json({ error: '返回的评查数据格式不正确' }, { status: 500 }); + } } catch (error) { console.error('获取评查数据失败:', error); return Response.json({ error: '获取评查数据失败' }, { status: 500 }); @@ -208,6 +210,7 @@ export default function ReviewDetails() { const [activeTab, setActiveTab] = useState('preview'); // 'preview', 'analysis', 'fileinfo' const [reviewData, setReviewData] = useState(null); const [activeReviewPointId, setActiveReviewPointId] = useState(null); + const [targetPage, setTargetPage] = useState(undefined); // 模拟获取评查数据 useEffect(() => { @@ -238,7 +241,7 @@ export default function ReviewDetails() { reviewPoints: reviewPoints, aiAnalysis: getMockReviewData().aiAnalysis, }; - console.log("reviewDataObj-------",reviewDataObj); + setReviewData(reviewDataObj); setIsLoading(false); @@ -248,22 +251,130 @@ export default function ReviewDetails() { setActiveTab(tabKey); }; - const handleReviewPointSelect = (reviewPointId: string) => { + const handleReviewPointSelect = (reviewPointId: string, page?: number) => { setActiveReviewPointId(reviewPointId); + setTargetPage(page); }; - const handleReviewPointStatusChange = (reviewPointId: string, newStatus: string) => { - // 更新评查点状态 - if (reviewData) { - const updatedReviewPoints = reviewData.reviewPoints.map(point => - point.id === reviewPointId ? { ...point, status: newStatus } : point - ); + // 刷新评审数据 + async function refreshReviewData(documentId: string) { + // 设置加载状态 + setIsLoading(true); + try { + // 获取最新的评审数据 + const response = await getReviewPoints(documentId); - setReviewData({ - ...reviewData, - reviewPoints: updatedReviewPoints, - statistics: calculateStatistics(updatedReviewPoints) + if ('error' in response && response.error) { + console.error('刷新评审数据失败:', response.error); + alert(`刷新评审数据失败: ${response.error}`); + return; + } + + // 确保response有效且具有预期的属性 + if ('data' in response && 'stats' in response && 'reviewInfo' in response) { + const reviewPointsData = response.data || []; + const statisticsData = response.stats || { total: 0, success: 0, warning: 0, error: 0, score: 0 }; + const reviewInfoData = response.reviewInfo || { + reviewTime: '', + reviewModel: '', + ruleGroup: '', + result: '', + issueCount: 0 + }; + + // 更新评审数据和统计信息 + setReviewData(prevData => { + if (!prevData) { + // 如果prevData为null,创建一个新的ReviewData对象 + return { + fileInfo: { + fileName: document?.name || "", + contractNumber: document?.documentNumber || "", + fileSize: document?.size ? formatFileSize(document.size) : "", + fileFormat: document?.fileType ? document.fileType.toUpperCase() : "", + pageCount: document?.pageCount || 0, + uploadTime: document?.uploadTime || "", + uploadUser: document?.uploadUser || "", + auditStatus: document?.auditStatus || 0 + }, + contractInfo: getMockReviewData().contractInfo, + reviewInfo: reviewInfoData as ReviewInfo, + statistics: statisticsData as Statistics, + fileContent: getMockReviewData().fileContent, + reviewPoints: reviewPointsData as unknown as ReviewPoint[], + aiAnalysis: getMockReviewData().aiAnalysis + }; + } + + // 处理prevData非null的情况 + return { + ...prevData, + reviewPoints: reviewPointsData as unknown as ReviewPoint[], + statistics: statisticsData as Statistics, + reviewInfo: reviewInfoData as ReviewInfo + }; + }); + + alert('评审数据已更新'); + } else { + console.error('返回的数据格式不正确'); + alert('刷新评审数据失败: 返回的数据格式不正确'); + } + } catch (error) { + console.error('刷新评审数据失败:', error); + alert(`刷新评审数据失败: ${error instanceof Error ? error.message : '未知错误'}`); + } finally { + setIsLoading(false); + } + } + + // 处理评审点状态变更 + const handleReviewPointStatusChange = async (reviewPointResultId: string, newStatus: string, message: string) => { + // 将字符串的布尔值转换为布尔类型 + const boolResult = newStatus === 'true'; + + try { + // 调用 API 更新评查结果 + const response = await updateReviewResult(reviewPointResultId, boolResult, message); + + if (response.error) { + console.error('更新评查结果失败:', response.error); + alert(`更新评查结果失败: ${response.error}`); + return; + } + + console.log('评查点状态更新成功:', { + id: reviewPointResultId, + result: boolResult, + message: message }); + + // 更新本地状态 + if (reviewData) { + const updatedReviewPoints = reviewData.reviewPoints.map(point => + point.id === reviewPointResultId ? { + ...point, + result: boolResult, + status: boolResult ? 'success' : 'error', + message: message + } : point + ); + + // 更新 UI 状态 + setReviewData({ + ...reviewData, + reviewPoints: updatedReviewPoints, + // statistics: calculateStatistics(updatedReviewPoints) + }); + + // 从API获取最新数据刷新列表 + if (document && document.id) { + await refreshReviewData(document.id.toString()); + } + } + } catch (error) { + console.error('更新评查结果出错:', error); + alert('更新评查结果失败,请稍后重试'); } }; @@ -298,9 +409,10 @@ export default function ReviewDetails() { {/* 左侧:文件预览 */}
      @@ -341,25 +453,6 @@ export default function ReviewDetails() { ); } -// 计算评查统计数据 -function calculateStatistics(reviewPoints: ReviewPoint[]): Statistics { - const total = reviewPoints.length; - const success = reviewPoints.filter(point => point.status === 'success').length; - const warning = reviewPoints.filter(point => point.status === 'warning').length; - const error = reviewPoints.filter(point => point.status === 'error').length; - - // 计算评分:通过占总数的百分比,错误项有额外惩罚 - const score = Math.round((success / total) * 100 - (error * 5)); - - return { - total, - success, - warning, - error, - score: Math.max(0, Math.min(100, score)) // 确保分数在0-100之间 - }; -} - // 模拟评查数据 function getMockReviewData(): ReviewData { return { diff --git a/app/routes/rule-groups._index.tsx b/app/routes/rule-groups._index.tsx index 99a6371..ccef900 100644 --- a/app/routes/rule-groups._index.tsx +++ b/app/routes/rule-groups._index.tsx @@ -443,7 +443,7 @@ export default function RuleGroupsIndex() { { title: "分组名称", key: "name", - width: "400px", + width: "35%", render: (_: unknown, record: RuleGroup & { isParent?: boolean, parentId?: string }) => (
      {record.isParent && ( @@ -467,7 +467,7 @@ export default function RuleGroupsIndex() { )} {record.name} @@ -488,10 +488,11 @@ export default function RuleGroupsIndex() { { title: "评查点数量", key: "ruleCount", + width: "12%", render: (_: unknown, record: RuleGroup & { isParent?: boolean, parentId?: string }) => ( - - {calculateTotalRuleCount(record)} - + ) }, { @@ -508,6 +509,7 @@ export default function RuleGroupsIndex() { { title: "创建时间", key: "createdAt", + width: "15%", render: (_: unknown, record: RuleGroup) => ( {record.createdAt || '-'} ) diff --git a/app/routes/rules-files.tsx b/app/routes/rules-files.tsx index 12bd64c..afa0c75 100644 --- a/app/routes/rules-files.tsx +++ b/app/routes/rules-files.tsx @@ -1,5 +1,5 @@ import { type MetaFunction, type LoaderFunctionArgs } from "@remix-run/node"; -import { useLoaderData, useSearchParams } from "@remix-run/react"; +import { useLoaderData, useSearchParams, useNavigate } from "@remix-run/react"; import { Button } from "~/components/ui/Button"; import { Card } from "~/components/ui/Card"; import { FileIcon } from "~/components/ui/FileIcon"; @@ -11,8 +11,8 @@ import { StatusBadge } from "~/components/ui/StatusBadge"; import rulesFilesStyles from "~/styles/pages/rules-files.css?url"; import { getReviewFiles, - updateReviewStatus, - type ReviewFileUI + type ReviewFileUI, + updateDocumentAuditStatus } from "~/api/evaluation_points/rules-files"; import { getDocumentTypes } from "~/api/document-types/document-types"; @@ -115,7 +115,8 @@ export function ErrorBoundary() { export default function RulesFiles() { const { files, documentTypes, totalCount, currentPage, pageSize } = useLoaderData(); const [searchParams, setSearchParams] = useSearchParams(); - + const navigate = useNavigate(); + // 处理筛选条件变更 const handleFilterChange = (e: React.ChangeEvent) => { const { name, value } = e.target; @@ -162,21 +163,114 @@ export default function RulesFiles() { newParams.set('page', '1'); // 改变每页条数时重置为第一页 setSearchParams(newParams); }; + + // 查看评查文件 + const handleReviewFileClick = async (fileId: string, auditStatus: number | null) => { + // 检查audit_status是否为0,如果是则更新为2 + if (auditStatus === 0) { + try { + const response = await updateDocumentAuditStatus(fileId, 2); + if (response.error) { + console.error('更新文件审核状态失败:', response.error); + // 尽管更新失败,仍然导航到文件详情页 + } + } catch (error) { + console.error('更新文件审核状态时出错:', error); + // 尽管发生错误,仍然导航到文件详情页 + } + } + + // 导航到评查详情页 + navigate(`/reviews?id=${fileId}`); + }; // 渲染问题摘要 const renderIssues = (file: ReviewFileUI) => { - // 如果评查状态为通过,显示"所有评查点均通过" - if (file.reviewStatus === 'pass') { - return ( -
      - 所有评查点均通过 -
      - ); + // 如果评查状态为通过(说明所有评查结果为true),显示"所有评查点均通过" + if (file.status === 'Processed') { + if (file.reviewStatus === 'pass') { + return ( +
      + 所有评查点均通过 +
      + ); + } + + // 如果评查状态为通过,显示"所有评查点均通过" + if (file.reviewStatus === 'fail') { + return ( +
      + 统计分数为:{file.score || 0}。分数低于80分。 +
      + ); + } + + // 显示问题列表 + if (file.reviewStatus !== 'pass' && file.reviewStatus !== 'fail' && file.issues && file.issues.length > 0) { + // 最多显示2个问题 + const displayIssues = file.issues.slice(0, 2); + + return ( +
      + {displayIssues.map((issue, index) => ( +
      + + {issue.message} +
      + ))} + + {file.issues.length > 2 && ( +
      + 还有 {file.issues.length - 2} 个问题... +
      + )} +
      + ); + } } - // 其他状态显示占位符 return
      -
      ; }; + + + // 下载文件 + const handleDownload = async (path: string) => { + try { + const urlBefore = 'http://172.18.0.100:9000/docauditai/'; + const downloadUrl = `${urlBefore}${path}`; + + // 使用fetch获取文件内容 + const response = await fetch(downloadUrl); + if (!response.ok) { + throw new Error(`下载失败: ${response.status} ${response.statusText}`); + } + + // 将响应转换为Blob + const blob = await response.blob(); + + // 创建Blob URL + const blobUrl = URL.createObjectURL(blob); + + // 创建一个隐藏的a标签并点击它 + const a = document.createElement('a'); + a.style.display = 'none'; + a.href = blobUrl; + // 从路径中获取文件名 + const fileName = path.split('/').pop() || 'document'; + a.download = decodeURIComponent(fileName); + document.body.appendChild(a); + a.click(); + + // 清理 + setTimeout(() => { + document.body.removeChild(a); + URL.revokeObjectURL(blobUrl); + }, 100); + } catch (error) { + console.error('下载文件失败:', error); + alert(`下载文件失败: ${error instanceof Error ? error.message : '未知错误'}`); + } + }; // 文件类型选项 const fileTypeOptions = documentTypes.map((type: {id: number, name: string}) => ({ @@ -197,7 +291,7 @@ export default function RulesFiles() { { value: DateRange.TODAY, label: '今天' }, { value: DateRange.WEEK, label: '本周' }, { value: DateRange.MONTH, label: '本月' }, - { value: DateRange.CUSTOM, label: '自定义时间段' } + // { value: DateRange.CUSTOM, label: '自定义时间段' } ]; // 定义表格列配置 @@ -252,13 +346,18 @@ export default function RulesFiles() { title: "评查状态", key: "reviewStatus", width: "12%", - render: (_: unknown, file: ReviewFileUI) => ( - - ) + render: (_: unknown, file: ReviewFileUI) => + file.status === 'Processed' ? ( + 0?'('+file.issueCount+')':''}`} + showIcon={true} + /> + ) : ( +
      + - +
      + ) }, { title: "问题摘要", @@ -276,13 +375,15 @@ export default function RulesFiles() { type="default" size="small" icon="ri-eye-line" - to={`/reviews?id=${file.id}`} + // to={`/reviews?id=${file.id}`} + onClick={() => handleReviewFileClick(file.id, file.auditStatus)} + disabled={file.status !== 'Processed'} className="mr-2" > 查看 - @@ -344,8 +445,8 @@ export default function RulesFiles() { options={[ { value: "upload_time_desc", label: "上传时间 ↓" }, { value: "upload_time_asc", label: "上传时间 ↑" }, - { value: "issue_count_desc", label: "问题数量 ↓" }, - { value: "issue_count_asc", label: "问题数量 ↑" } + // { value: "issue_count_desc", label: "问题数量 ↓" }, + // { value: "issue_count_asc", label: "问题数量 ↑" } ]} /> diff --git a/app/styles/pages/home.css b/app/styles/pages/home.css index 8cb37b8..351207f 100644 --- a/app/styles/pages/home.css +++ b/app/styles/pages/home.css @@ -4,7 +4,7 @@ /* 仪表盘容器 */ .dashboard-container { - @apply p-5; + @apply p-2; } /* 统计卡片 */ diff --git a/app/styles/reviews.css b/app/styles/reviews.css index 22d1be2..538e0da 100644 --- a/app/styles/reviews.css +++ b/app/styles/reviews.css @@ -11,6 +11,10 @@ --bg-gray: #f5f5f5; } +.container { + width: 100%; +} + /* 文件信息和操作按钮区域 */ .file-info-header { margin-bottom: 16px; @@ -391,4 +395,16 @@ .info-value { flex: 1; -} \ No newline at end of file +} + +.text-success { + color: #52c41a; +} + +@media (max-width: 768px) { + .container { + width: 100%; + } +} + + diff --git a/html/index.html b/html/index.html index f8a3366..080207e 100644 --- a/html/index.html +++ b/html/index.html @@ -4,8 +4,10 @@ 中国烟草AI合同及卷宗审核系统 - 首页 - - + + +