完善评查详情
This commit is contained in:
@@ -717,8 +717,7 @@ export async function updateDocumentType(id: string, documentType: DocumentTypeU
|
|||||||
name: documentType.name.trim(),
|
name: documentType.name.trim(),
|
||||||
description: documentType.description || '',
|
description: documentType.description || '',
|
||||||
evaluation_point_groups_ids: groupIds,
|
evaluation_point_groups_ids: groupIds,
|
||||||
prompt_config: promptConfig,
|
prompt_config: promptConfig
|
||||||
code: documentType.code || null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('更新文档类型请求数据:', JSON.stringify(apiDocumentType, null, 2));
|
console.log('更新文档类型请求数据:', JSON.stringify(apiDocumentType, null, 2));
|
||||||
|
|||||||
@@ -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';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -95,6 +96,13 @@ interface StatsData {
|
|||||||
* @returns 评查点结果列表和统计数据
|
* @returns 评查点结果列表和统计数据
|
||||||
*/
|
*/
|
||||||
export async function getReviewPoints(fileId: string) {
|
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表
|
// 步骤1:根据fileId查询evaluation_results表
|
||||||
const evaluationResultsParams: PostgrestParams = {
|
const evaluationResultsParams: PostgrestParams = {
|
||||||
select: '*',
|
select: '*',
|
||||||
@@ -195,16 +203,81 @@ export async function getReviewPoints(fileId: string) {
|
|||||||
data = result.evaluated_results.data || '';
|
data = result.evaluated_results.data || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 提取页码数组
|
||||||
|
let contentPage: number[] = [];
|
||||||
|
console.log('datacontent-------', data);
|
||||||
|
if (data && typeof data === 'object') {
|
||||||
|
try {
|
||||||
|
const dataObj = data as Record<string, string>;
|
||||||
|
// 检查是否是预期的格式 {'立案报告表-完整性检查':'缺失部分内容'}
|
||||||
|
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<string, any> = documentData.data.ocrResult as Record<string, any>;
|
||||||
|
|
||||||
|
// 检查是否有嵌套的ocr_result对象
|
||||||
|
if ('ocr_result' in ocrData &&
|
||||||
|
ocrData.ocr_result &&
|
||||||
|
typeof ocrData.ocr_result === 'object') {
|
||||||
|
ocrData = ocrData.ocr_result as Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
return {
|
||||||
id: result.id,
|
id: result.id,
|
||||||
title: message,
|
title: message,
|
||||||
pointName: point.name || '',
|
pointName: point.name || '',
|
||||||
groupName: group.name || '',
|
groupName: group.name || '',
|
||||||
status: point.suggestion_message_type || '',
|
|
||||||
|
status: point.suggestion_message_type || '', //评查点的评查结果状态
|
||||||
|
// status: 'error', //评查点的评查结果状态
|
||||||
|
|
||||||
content: data,
|
content: data,
|
||||||
|
|
||||||
|
contentPage: contentPage,
|
||||||
|
|
||||||
suggestion: point.suggestion_message || '',
|
suggestion: point.suggestion_message || '',
|
||||||
|
// suggestion: '只是给建议的修改内容',
|
||||||
|
|
||||||
result: result.evaluated_results?.result, // 记录评查结果,用于统计
|
result: result.evaluated_results?.result, // 记录评查结果,用于统计
|
||||||
score: point.score || 0,
|
score: point.score || 0,
|
||||||
|
|
||||||
|
postAction: point.post_action || '',
|
||||||
|
// postAction: 'manual',
|
||||||
|
|
||||||
|
actionContent: point.action_config || '',
|
||||||
|
// actionContent: '用户提前在评查点输入过的修改内容',
|
||||||
|
|
||||||
legalBasis: point.references_laws || {}
|
legalBasis: point.references_laws || {}
|
||||||
// legalBasis: {
|
// legalBasis: {
|
||||||
// name: '中华人民共和国食品安全法',
|
// name: '中华人民共和国食品安全法',
|
||||||
@@ -271,5 +344,79 @@ export async function getReviewPoints(fileId: string) {
|
|||||||
};
|
};
|
||||||
// console.log("reviewInfo-------",JSON.stringify(reviewInfo,null,2));
|
// 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<EvaluationResult[]>(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<unknown, typeof updatedData>(
|
||||||
|
'evaluation_results',
|
||||||
|
updatedData,
|
||||||
|
{ id: resultId }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
return { error: response.error, status: response.status };
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractedData = extractApiData<unknown>(response.data);
|
||||||
|
|
||||||
|
if (!extractedData) {
|
||||||
|
return { error: '更新评查结果失败', status: 500 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data: extractedData };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新评查结果失败:', error);
|
||||||
|
return {
|
||||||
|
error: error instanceof Error ? error.message : '更新评查结果失败',
|
||||||
|
status: 500
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ export interface DocumentType {
|
|||||||
// 评查文件UI接口
|
// 评查文件UI接口
|
||||||
export interface ReviewFileUI {
|
export interface ReviewFileUI {
|
||||||
id: string;
|
id: string;
|
||||||
|
status: string;
|
||||||
|
path: string;
|
||||||
fileName: string;
|
fileName: string;
|
||||||
fileCode: string;
|
fileCode: string;
|
||||||
fileType: string;
|
fileType: string;
|
||||||
@@ -59,6 +61,8 @@ export interface ReviewFileUI {
|
|||||||
reviewStatus: string;
|
reviewStatus: string;
|
||||||
reviewStatusCode: number;
|
reviewStatusCode: number;
|
||||||
issueCount: number;
|
issueCount: number;
|
||||||
|
score?: number;
|
||||||
|
auditStatus: number | null;
|
||||||
issues: Array<{
|
issues: Array<{
|
||||||
severity: 'info' | 'warning' | 'error' | 'critical';
|
severity: 'info' | 'warning' | 'error' | 'critical';
|
||||||
message: string;
|
message: string;
|
||||||
@@ -77,6 +81,35 @@ export interface DocumentSearchParams {
|
|||||||
pageSize?: number; // 每页条数
|
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 日期字符串
|
* @param dateString 日期字符串
|
||||||
@@ -120,7 +153,7 @@ function extractApiData<T>(responseData: unknown): T | null {
|
|||||||
export function mapReviewStatusToUI(status: number | null): string {
|
export function mapReviewStatusToUI(status: number | null): string {
|
||||||
switch(status) {
|
switch(status) {
|
||||||
case 1: return 'pass';
|
case 1: return 'pass';
|
||||||
case 2: return 'warning';
|
case -2: return 'warning';
|
||||||
case -1: return 'fail';
|
case -1: return 'fail';
|
||||||
case 0: return 'pending';
|
case 0: return 'pending';
|
||||||
default: return 'pending';
|
default: return 'pending';
|
||||||
@@ -135,7 +168,7 @@ export function mapReviewStatusToUI(status: number | null): string {
|
|||||||
export function mapUIToReviewStatus(status: string): number {
|
export function mapUIToReviewStatus(status: string): number {
|
||||||
switch(status) {
|
switch(status) {
|
||||||
case 'pass': return 1;
|
case 'pass': return 1;
|
||||||
case 'warning': return 2;
|
case 'warning': return -2;
|
||||||
case 'fail': return -1;
|
case 'fail': return -1;
|
||||||
case 'pending': return 0;
|
case 'pending': return 0;
|
||||||
default: return 0;
|
default: return 0;
|
||||||
@@ -162,6 +195,8 @@ export function convertToReviewFileUI(document: Document, documentTypeName: stri
|
|||||||
|
|
||||||
const reviewFileUI: ReviewFileUI = {
|
const reviewFileUI: ReviewFileUI = {
|
||||||
id: document.id.toString(),
|
id: document.id.toString(),
|
||||||
|
status: document.status,
|
||||||
|
path: document.path,
|
||||||
fileName: document.name,
|
fileName: document.name,
|
||||||
fileCode: document.document_number,
|
fileCode: document.document_number,
|
||||||
fileType: documentTypeName,
|
fileType: documentTypeName,
|
||||||
@@ -171,6 +206,8 @@ export function convertToReviewFileUI(document: Document, documentTypeName: stri
|
|||||||
reviewStatus: reviewStatus,
|
reviewStatus: reviewStatus,
|
||||||
reviewStatusCode: document.evaluations_status || 0,
|
reviewStatusCode: document.evaluations_status || 0,
|
||||||
issueCount: 0,
|
issueCount: 0,
|
||||||
|
score: 0,
|
||||||
|
auditStatus: document.audit_status,
|
||||||
issues: [],
|
issues: [],
|
||||||
createdBy: document.user_id?.toString() || '系统'
|
createdBy: document.user_id?.toString() || '系统'
|
||||||
};
|
};
|
||||||
@@ -301,12 +338,175 @@ export async function getReviewFiles(searchParams: DocumentSearchParams = {}): P
|
|||||||
typeNameMap[type.id] = type.name;
|
typeNameMap[type.id] = type.name;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 将文档数据转换为UI文件对象
|
// 获取评查文件的评查结果
|
||||||
const reviewFiles = extractedDocuments.map(doc => {
|
// 第一步:收集所有文档ID
|
||||||
const typeName = typeNameMap[doc.type_id] || '未知类型';
|
const documentIds = extractedDocuments.map(doc => doc.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<EvaluationResult[]>(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<EvaluationPoint[]>(evaluationPointsResponse.data) || [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建评查点ID到评查点数据的映射
|
||||||
|
const pointsMap = new Map<string | number, EvaluationPoint>();
|
||||||
|
evaluationPoints.forEach(point => {
|
||||||
|
pointsMap.set(point.id, point);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 创建文档ID到评查结果列表的映射
|
||||||
|
const documentResultsMap = new Map<string | number, EvaluationResult[]>();
|
||||||
|
evaluationResults.forEach(result => {
|
||||||
|
const docId = result.document_id;
|
||||||
|
if (!documentResultsMap.has(docId)) {
|
||||||
|
documentResultsMap.set(docId, []);
|
||||||
|
}
|
||||||
|
documentResultsMap.get(docId)!.push(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算每个文档的评查状态和问题列表
|
||||||
|
const documentStatusMap = new Map<string | number, DocumentReviewResult>();
|
||||||
|
// 存储每个文档的问题消息
|
||||||
|
const documentIssuesMap = new Map<string | number, Array<{severity: 'info' | 'warning' | 'error' | 'critical', message: string}>>();
|
||||||
|
// 存储每个文档的分数
|
||||||
|
const documentScoreMap = new Map<string | number, number>();
|
||||||
|
|
||||||
|
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 {
|
return {
|
||||||
data: {
|
data: {
|
||||||
files: reviewFiles,
|
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<Document, Partial<Document>>(
|
||||||
|
'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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ export interface DocumentUI {
|
|||||||
isTest: boolean;
|
isTest: boolean;
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
pageCount?: number;
|
pageCount?: number;
|
||||||
|
ocrResult?: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -175,7 +176,8 @@ async function convertToUIDocument(doc: Document): Promise<DocumentUI> {
|
|||||||
path: doc.path,
|
path: doc.path,
|
||||||
isTest: doc.is_test_document,
|
isTest: doc.is_test_document,
|
||||||
updatedAt: formatDate(doc.updated_at),
|
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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,249 @@
|
|||||||
|
import { postgrestGet, type PostgrestParams } from "../postgrest-client";
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从不同格式的 API 响应中提取数据
|
||||||
|
* @param responseData API 响应数据
|
||||||
|
* @returns 提取后的数据或 null
|
||||||
|
*/
|
||||||
|
function extractApiData<T>(responseData: unknown): T | null {
|
||||||
|
if (!responseData) return null;
|
||||||
|
|
||||||
|
// 格式1: { code: number, msg: string, data: T }
|
||||||
|
if (typeof responseData === 'object' && responseData !== null &&
|
||||||
|
'code' in responseData &&
|
||||||
|
'data' in responseData &&
|
||||||
|
(responseData as { data: unknown }).data) {
|
||||||
|
return (responseData as { data: T }).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式2: 直接是数据对象
|
||||||
|
return responseData as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评估结果类型定义
|
||||||
|
*/
|
||||||
|
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<HomeStatistics> {
|
||||||
|
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 }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -37,24 +37,12 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) {
|
|||||||
path: '/files/upload',
|
path: '/files/upload',
|
||||||
icon: 'ri-upload-cloud-line'
|
icon: 'ri-upload-cloud-line'
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// id: 'file-list',
|
|
||||||
// title: '文件列表',
|
|
||||||
// path: '/files',
|
|
||||||
// icon: 'ri-file-list-3-line'
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
id:'documents',
|
id:'documents',
|
||||||
title:'文档列表',
|
title:'文档列表',
|
||||||
path:'/documents',
|
path:'/documents',
|
||||||
icon:'ri-file-list-3-line'
|
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',
|
path: '/rules-new',
|
||||||
icon: 'ri-add-circle-line'
|
icon: 'ri-add-circle-line'
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
id: 'review-detail',
|
// id: 'review-detail',
|
||||||
title: '评查详情',
|
// title: '评查详情',
|
||||||
path: '/reviews',
|
// path: '/reviews',
|
||||||
icon: 'ri-file-chart-line'
|
// icon: 'ri-file-chart-line'
|
||||||
}
|
// }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export function FileInfo({ fileInfo, onConfirmResults }: FileInfoProps) {
|
|||||||
</span>
|
</span>
|
||||||
{fileInfo.fileSize && (
|
{fileInfo.fileSize && (
|
||||||
<span className="text-xs text-gray-500 ml-2">
|
<span className="text-xs text-gray-500 ml-2">
|
||||||
{fileInfo.fileSize} | {fileInfo.fileFormat} | {fileInfo.pageCount}页
|
| {fileInfo.fileSize} | {fileInfo.fileFormat} | {fileInfo.pageCount}页
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{fileInfo.uploadTime && (
|
{fileInfo.uploadTime && (
|
||||||
|
|||||||
@@ -3,6 +3,11 @@
|
|||||||
* 显示文档内容和评查点高亮
|
* 显示文档内容和评查点高亮
|
||||||
*/
|
*/
|
||||||
import { useState, useEffect, useRef } from 'react';
|
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类型
|
// 导入统一的ReviewPoint类型
|
||||||
import { type ReviewPoint } from './';
|
import { type ReviewPoint } from './';
|
||||||
@@ -11,6 +16,12 @@ import { type ReviewPoint } from './';
|
|||||||
interface FileContent {
|
interface FileContent {
|
||||||
title: string;
|
title: string;
|
||||||
contractNumber: string;
|
contractNumber: string;
|
||||||
|
path: string;
|
||||||
|
ocrResult?: {
|
||||||
|
__meta?: {
|
||||||
|
page_offset?: number;
|
||||||
|
};
|
||||||
|
}; // 添加ocrResult属性
|
||||||
parties: {
|
parties: {
|
||||||
partyA: {
|
partyA: {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -35,12 +46,15 @@ interface FilePreviewProps {
|
|||||||
fileContent: FileContent;
|
fileContent: FileContent;
|
||||||
reviewPoints: ReviewPoint[];
|
reviewPoints: ReviewPoint[];
|
||||||
activeReviewPointId: string | null;
|
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 [zoomLevel, setZoomLevel] = useState(100);
|
||||||
const [highlightsVisible, setHighlightsVisible] = useState(true);
|
const [highlightsVisible, setHighlightsVisible] = useState(true);
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [numPages, setNumPages] = useState<number | null>(null);
|
||||||
|
const [loadError, setLoadError] = useState<string | null>(null);
|
||||||
|
|
||||||
// 放大文档
|
// 放大文档
|
||||||
const handleZoomIn = () => {
|
const handleZoomIn = () => {
|
||||||
@@ -77,6 +91,28 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointId }:
|
|||||||
}
|
}
|
||||||
}, [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) => {
|
const getHighlightClass = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@@ -91,67 +127,112 @@ 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(
|
||||||
|
<div key={i} id={`page-${i}`} className="mb-6">
|
||||||
|
{/* 页码标识,显示在页面上方 */}
|
||||||
|
<div className="text-center text-gray-500 text-sm mb-2">第 {i} 页</div>
|
||||||
|
|
||||||
|
{/* 页面容器,应用缩放变换,设置相对定位用于放置评查点高亮 */}
|
||||||
|
<div style={{
|
||||||
|
transform: `scale(${zoomLevel / 100})`, // 根据zoomLevel应用缩放
|
||||||
|
transformOrigin: 'top center', // 缩放原点设置为顶部中心
|
||||||
|
position: 'relative' // 相对定位,作为评查点高亮的定位参考
|
||||||
|
}}>
|
||||||
|
{/* 渲染PDF页面组件 */}
|
||||||
|
<Page
|
||||||
|
pageNumber={i} // 当前页码
|
||||||
|
renderTextLayer={true} // 启用文本层,使文本可选择
|
||||||
|
renderAnnotationLayer={true} // 启用注释层,显示PDF内置注释
|
||||||
|
className="border border-gray-300 shadow-md" // 添加边框和阴影样式
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 渲染评查点高亮区域 */}
|
||||||
|
{highlightsVisible && pageReviewPoints.map(point => {
|
||||||
|
// 判断当前评查点是否为激活状态(被选中)
|
||||||
|
const isActive = point.id === activeReviewPointId;
|
||||||
|
|
||||||
|
return (
|
||||||
|
// 评查点高亮区域
|
||||||
|
<div
|
||||||
|
key={point.id}
|
||||||
|
// 添加多个类名:基础高亮区域、PDF专用高亮、状态样式类、激活状态类
|
||||||
|
className={`highlight-area pdf-highlight ${getHighlightClass(point.status)} ${isActive ? 'highlight-focus' : ''}`}
|
||||||
|
// 设置唯一标识,用于滚动定位和DOM查询
|
||||||
|
data-review-id={point.id}
|
||||||
|
// 设置高亮区域的样式
|
||||||
|
style={{
|
||||||
|
position: 'absolute', // 绝对定位,相对于页面容器
|
||||||
|
zIndex: isActive ? 20 : 10, // 激活状态时提高层级,确保在最上层显示
|
||||||
|
boxShadow: isActive ? '0 0 0 2px yellow, 0 0 10px rgba(0,0,0,0.3)' : '', // 激活时添加特殊阴影效果
|
||||||
|
// 根据评查点的位置信息计算高亮区域位置,实际项目中需根据真实位置坐标计算
|
||||||
|
top: `${point.position?.index ? point.position.index * 20 : 20}px`,
|
||||||
|
left: '50px',
|
||||||
|
width: '300px',
|
||||||
|
height: '30px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回所有页面组件数组
|
||||||
|
return pages;
|
||||||
|
};
|
||||||
|
|
||||||
// 渲染文档内容
|
// 渲染文档内容
|
||||||
const renderDocumentContent = () => {
|
const renderDocumentContent = () => {
|
||||||
return (
|
return (
|
||||||
<div className="word-document" ref={contentRef} style={{transform: `scale(${zoomLevel/100})`, transformOrigin: 'center top'}}>
|
<Document
|
||||||
<h1>{fileContent.title}</h1>
|
file={'http://172.18.0.100:9000/docauditai/'+fileContent.path}
|
||||||
<p style={{textAlign: 'right'}}>合同编号:{fileContent.contractNumber}</p>
|
onLoadSuccess={onDocumentLoadSuccess}
|
||||||
|
onLoadError={(error) => {
|
||||||
<div style={{margin: '20px 0'}}>
|
console.error("PDF加载错误:", error);
|
||||||
<p><strong>甲方(供方):</strong>{fileContent.parties.partyA.name}</p>
|
setLoadError("PDF文档加载失败:" + (error.message || "未知错误"));
|
||||||
<p>地址:{fileContent.parties.partyA.address}</p>
|
}}
|
||||||
<p>法定代表人:{fileContent.parties.partyA.representative}</p>
|
className="flex flex-col items-center"
|
||||||
<p>联系电话:{fileContent.parties.partyA.phone}</p>
|
error={<div className="text-red-500">PDF文档加载失败,请检查链接或网络连接。</div>}
|
||||||
<p> </p>
|
noData={<div>无数据</div>}
|
||||||
<p><strong>乙方(需方):</strong>{fileContent.parties.partyB.name}</p>
|
loading={<div className="text-center py-10">PDF加载中...</div>}
|
||||||
<p>地址:{fileContent.parties.partyB.address}</p>
|
|
||||||
<p>法定代表人:{fileContent.parties.partyB.representative}</p>
|
|
||||||
<p>联系电话:{fileContent.parties.partyB.phone}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>根据《中华人民共和国合同法》及有关法律法规的规定,经双方协商一致,签订本合同,共同遵守。</p>
|
|
||||||
|
|
||||||
{fileContent.sections.map((section, sectionIndex) => (
|
|
||||||
<div key={sectionIndex}>
|
|
||||||
<h2>{section.title}</h2>
|
|
||||||
{renderSectionContent(section.content, section.title, sectionIndex)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 渲染章节内容,处理高亮
|
|
||||||
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 (
|
|
||||||
<div
|
|
||||||
key={`${sectionIndex}-${lineIndex}`}
|
|
||||||
className={`highlight-area ${getHighlightClass(reviewPoint.status)} ${isActive ? 'highlight-focus' : ''}`}
|
|
||||||
data-review-id={reviewPoint.id}
|
|
||||||
style={isActive ? {zIndex: 20, boxShadow: '0 0 0 2px yellow, 0 0 10px rgba(0,0,0,0.3)'} : {}}
|
|
||||||
>
|
>
|
||||||
<p>{line}</p>
|
{renderAllPages()}
|
||||||
</div>
|
</Document>
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
// 没有评查点,正常显示
|
|
||||||
return <p key={`${sectionIndex}-${lineIndex}`}>{line}</p>;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -182,8 +263,14 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointId }:
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="file-preview-content">
|
<div className="file-preview-content" ref={contentRef}>
|
||||||
{renderDocumentContent()}
|
{loadError ? (
|
||||||
|
<div className="text-red-500 p-4">
|
||||||
|
<p>{loadError}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
renderDocumentContent()
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export interface ReviewPoint {
|
|||||||
humanReviewNote?: string;
|
humanReviewNote?: string;
|
||||||
humanReviewBy?: string;
|
humanReviewBy?: string;
|
||||||
humanReviewTime?: string;
|
humanReviewTime?: string;
|
||||||
|
contentPage?: number[];
|
||||||
position?: {
|
position?: {
|
||||||
section: string;
|
section: string;
|
||||||
index: number;
|
index: number;
|
||||||
@@ -45,6 +46,8 @@ export interface ReviewPoint {
|
|||||||
articles?: Array<string | { name?: string; content?: string; [key: string]: unknown }>;
|
articles?: Array<string | { name?: string; content?: string; [key: string]: unknown }>;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
|
postAction?: string;
|
||||||
|
actionContent?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 统计数据类型
|
// 统计数据类型
|
||||||
@@ -60,8 +63,8 @@ interface ReviewPointsListProps {
|
|||||||
reviewPoints: ReviewPoint[];
|
reviewPoints: ReviewPoint[];
|
||||||
statistics: Statistics;
|
statistics: Statistics;
|
||||||
activeReviewPointId: string | null;
|
activeReviewPointId: string | null;
|
||||||
onReviewPointSelect: (id: string) => void;
|
onReviewPointSelect: (id: string, page?: number) => void;
|
||||||
onStatusChange: (id: string, status: string) => void;
|
onStatusChange: (id: string, status: string, message: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ReviewPointsList({
|
export function ReviewPointsList({
|
||||||
@@ -73,24 +76,35 @@ export function ReviewPointsList({
|
|||||||
}: ReviewPointsListProps) {
|
}: ReviewPointsListProps) {
|
||||||
// 状态管理
|
// 状态管理
|
||||||
const [editingReviewPoint, setEditingReviewPoint] = useState<string | null>(null); // 当前正在编辑的评查点ID
|
const [editingReviewPoint, setEditingReviewPoint] = useState<string | null>(null); // 当前正在编辑的评查点ID
|
||||||
const [userInputText, setUserInputText] = useState(''); // 用户输入的审核意见文本
|
|
||||||
const [searchText, setSearchText] = useState(''); // 搜索文本
|
const [searchText, setSearchText] = useState(''); // 搜索文本
|
||||||
const [statusFilter, setStatusFilter] = useState<string | null>(null); // 状态过滤
|
const [statusFilter, setStatusFilter] = useState<string | null>(null); // 状态过滤
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
const [suggestionTexts, setSuggestionTexts] = useState<Record<string, string>>({}); // 存储每个评查点的建议文本
|
const [suggestionTexts, setSuggestionTexts] = useState<Record<string, string>>({}); // 存储每个评查点的建议文本
|
||||||
|
|
||||||
|
// 添加重新审核意见的状态/ 用户输入的修改内容 / 用户提前写好的修改内容
|
||||||
|
const [manualReviewNotes, setManualReviewNotes] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
// 初始化建议文本
|
// 初始化建议文本
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 将所有评查点的建议文本存储到状态中
|
// 将所有评查点的建议文本存储到状态中
|
||||||
const suggestions: Record<string, string> = {};
|
const suggestions: Record<string, string> = {};
|
||||||
|
|
||||||
reviewPoints.forEach(point => {
|
reviewPoints.forEach(point => {
|
||||||
suggestions[point.id] = point.suggestion || '';
|
suggestions[point.id] = point.suggestion || '';
|
||||||
});
|
});
|
||||||
setSuggestionTexts(suggestions);
|
setSuggestionTexts(suggestions);
|
||||||
|
|
||||||
|
// 使用函数式更新,不再需要外部 manualReviewNotes 变量
|
||||||
|
setManualReviewNotes(prev => {
|
||||||
|
const notes = {...prev};
|
||||||
|
reviewPoints.forEach(point => {
|
||||||
|
notes[point.id] = point.actionContent || '';
|
||||||
|
});
|
||||||
|
return notes;
|
||||||
|
});
|
||||||
}, [reviewPoints]);
|
}, [reviewPoints]);
|
||||||
|
|
||||||
// 处理建议文本变更
|
// 处理建议文本变更
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
const handleSuggestionChange = (reviewPointId: string, text: string) => {
|
const handleSuggestionChange = (reviewPointId: string, text: string) => {
|
||||||
setSuggestionTexts(prev => ({
|
setSuggestionTexts(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -98,6 +112,30 @@ 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}`);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 过滤评查点
|
* 过滤评查点
|
||||||
* 根据搜索文本和状态过滤条件筛选评查点
|
* 根据搜索文本和状态过滤条件筛选评查点
|
||||||
@@ -146,36 +184,6 @@ export function ReviewPointsList({
|
|||||||
// onStatusChange(reviewPointId, 'success');
|
// 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({
|
|||||||
{/* 总计数量 */}
|
{/* 总计数量 */}
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<button
|
<button
|
||||||
className={`w-7 h-7 bg-gray-100 rounded-md flex items-center justify-center cursor-pointer ${statusFilter === null && searchText === '' ? 'ring-2 ring-gray-400' : ''}`}
|
className={`px-3 h-7 bg-gray-100 rounded-md flex items-center justify-center cursor-pointer ${statusFilter === null && searchText === '' ? 'ring-2 ring-gray-400' : ''}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setStatusFilter(null);
|
setStatusFilter(null);
|
||||||
setSearchText('');
|
setSearchText('');
|
||||||
@@ -224,47 +232,47 @@ export function ReviewPointsList({
|
|||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span className="text-sm font-semibold text-gray-600">{totalToShow}</span>
|
<span className="text-sm font-semibold text-gray-600">{totalToShow}</span>
|
||||||
</button>
|
|
||||||
<span className="text-xs text-gray-500 ml-1">总计</span>
|
<span className="text-xs text-gray-500 ml-1">总计</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-8 border-r border-gray-200"></div>
|
<div className="h-8 border-r border-gray-200"></div>
|
||||||
{/* 通过数量 */}
|
{/* 通过数量 */}
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<button
|
<button
|
||||||
className={`w-7 h-7 bg-green-50 rounded-md flex items-center justify-center cursor-pointer ${statusFilter === 'success' ? 'ring-2 ring-success' : ''}`}
|
className={`px-3 h-7 bg-green-50 rounded-md flex items-center justify-center cursor-pointer ${statusFilter === 'success' ? 'ring-2 ring-green-500' : ''}`}
|
||||||
onClick={() => setStatusFilter(statusFilter === 'success' ? null : 'success')}
|
onClick={() => setStatusFilter(statusFilter === 'success' ? null : 'success')}
|
||||||
aria-label={`过滤通过项 ${statusFilter === 'success' ? '(已选中)' : ''}`}
|
aria-label={`过滤通过项 ${statusFilter === 'success' ? '(已选中)' : ''}`}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span className="text-sm font-semibold text-success">{successToShow}</span>
|
<span className="text-sm font-semibold text-success">{successToShow}</span>
|
||||||
|
<span className="text-xs text-gray-500 ml-2">通过</span>
|
||||||
</button>
|
</button>
|
||||||
<span className="text-xs text-gray-500 ml-1">通过</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="h-8 border-r border-gray-200"></div>
|
<div className="h-8 border-r border-gray-200"></div>
|
||||||
{/* 警告数量 */}
|
{/* 警告数量 */}
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<button
|
<button
|
||||||
className={`w-7 h-7 bg-yellow-50 rounded-md flex items-center justify-center cursor-pointer ${statusFilter === 'warning' ? 'ring-2 ring-warning' : ''}`}
|
className={`px-3 h-7 bg-yellow-50 rounded-md flex items-center justify-center cursor-pointer ${statusFilter === 'warning' ? 'ring-2 ring-yellow-500' : ''}`}
|
||||||
onClick={() => setStatusFilter(statusFilter === 'warning' ? null : 'warning')}
|
onClick={() => setStatusFilter(statusFilter === 'warning' ? null : 'warning')}
|
||||||
aria-label={`过滤警告项 ${statusFilter === 'warning' ? '(已选中)' : ''}`}
|
aria-label={`过滤警告项 ${statusFilter === 'warning' ? '(已选中)' : ''}`}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span className="text-sm font-semibold text-warning">{warningToShow}</span>
|
<span className="text-sm font-semibold text-warning">{warningToShow}</span>
|
||||||
|
<span className="text-xs text-gray-500 ml-2">警告</span>
|
||||||
</button>
|
</button>
|
||||||
<span className="text-xs text-gray-500 ml-1">警告</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="h-8 border-r border-gray-200"></div>
|
<div className="h-8 border-r border-gray-200"></div>
|
||||||
{/* 错误数量 */}
|
{/* 错误数量 */}
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<button
|
<button
|
||||||
className={`w-7 h-7 bg-red-50 rounded-md flex items-center justify-center cursor-pointer ${statusFilter === 'error' ? 'ring-2 ring-error' : ''}`}
|
className={`px-3 h-7 bg-red-50 rounded-md flex items-center justify-center cursor-pointer ${statusFilter === 'error' ? 'ring-2 ring-red-500' : ''}`}
|
||||||
onClick={() => setStatusFilter(statusFilter === 'error' ? null : 'error')}
|
onClick={() => setStatusFilter(statusFilter === 'error' ? null : 'error')}
|
||||||
aria-label={`过滤错误项 ${statusFilter === 'error' ? '(已选中)' : ''}`}
|
aria-label={`过滤错误项 ${statusFilter === 'error' ? '(已选中)' : ''}`}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span className="text-sm font-semibold text-error">{errorToShow}</span>
|
<span className="text-sm font-semibold text-error">{errorToShow}</span>
|
||||||
|
<span className="text-xs text-gray-500 ml-2">错误</span>
|
||||||
</button>
|
</button>
|
||||||
<span className="text-xs text-gray-500 ml-1">错误</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -374,9 +382,9 @@ export function ReviewPointsList({
|
|||||||
* @returns 人工审核标记组件
|
* @returns 人工审核标记组件
|
||||||
*/
|
*/
|
||||||
const renderHumanReviewBadge = (reviewPoint: ReviewPoint) => {
|
const renderHumanReviewBadge = (reviewPoint: ReviewPoint) => {
|
||||||
if (reviewPoint.needsHumanReview) {
|
if (reviewPoint.postAction === 'manual') {
|
||||||
return (
|
return (
|
||||||
<span className="status-badge status-waiting ml-2">
|
<span className="status-badge status-waiting ml-2 mt-1 text-xs">
|
||||||
<i className="ri-user-line mr-1"></i>需人工
|
<i className="ri-user-line mr-1"></i>需人工
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@@ -411,6 +419,14 @@ export function ReviewPointsList({
|
|||||||
* @returns 评查点内容组件
|
* @returns 评查点内容组件
|
||||||
*/
|
*/
|
||||||
const renderReviewPointContent = (reviewPoint: ReviewPoint) => {
|
const renderReviewPointContent = (reviewPoint: ReviewPoint) => {
|
||||||
|
|
||||||
|
const handleManualReviewNotesChange = (reviewPointId: string, text: string) => {
|
||||||
|
setManualReviewNotes(prev => ({
|
||||||
|
...prev,
|
||||||
|
[reviewPointId]: text
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
// 如果当前评查点不处于编辑状态,只显示简单信息
|
// 如果当前评查点不处于编辑状态,只显示简单信息
|
||||||
if (editingReviewPoint !== reviewPoint.id) {
|
if (editingReviewPoint !== reviewPoint.id) {
|
||||||
// 根据result和status决定渲染哪种样式
|
// 根据result和status决定渲染哪种样式
|
||||||
@@ -423,19 +439,77 @@ export function ReviewPointsList({
|
|||||||
<p className="text-xs text-success"><i className="ri-check-line mr-1"></i>已处理</p>
|
<p className="text-xs text-success"><i className="ri-check-line mr-1"></i>已处理</p>
|
||||||
{reviewPoint.suggestion && (
|
{reviewPoint.suggestion && (
|
||||||
<div className="border-t border-green-200 mt-1 pt-1">
|
<div className="border-t border-green-200 mt-1 pt-1">
|
||||||
<p className="text-xs text-gray-600">{reviewPoint.suggestion}</p>
|
<p className="text-xs text-gray-600 select-text">{reviewPoint.suggestion}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理 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 (
|
||||||
|
<div className="mt-2">
|
||||||
|
{reviewPoint.suggestion && (
|
||||||
|
<div className="p-2 bg-blue-50 rounded border border-blue-200 text-xs mb-3 select-text">
|
||||||
|
<div className="flex items-start">
|
||||||
|
<i className="ri-information-line text-blue-500 mr-2 mt-0.5"></i>
|
||||||
|
<p className="text-xs text-gray-600 select-text">{reviewPoint.suggestion}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
{/* 额外的文本输入框区域 */}
|
||||||
|
<div className="mb-3">
|
||||||
|
<textarea
|
||||||
|
id={`manual-review-${reviewPoint.id}`}
|
||||||
|
className="w-full p-2 border rounded bg-white text-xs min-h-[80px] focus:outline-none focus:border-primary"
|
||||||
|
placeholder="请输入重新审核意见..."
|
||||||
|
value={manualReviewNotes[reviewPoint.id] || ''}
|
||||||
|
onChange={(e) => handleNoteChange(reviewPoint.id, e.target.value)}
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<button
|
||||||
|
className="bg-purple-600 hover:bg-purple-700 text-xs text-white py-1 px-2 rounded-md flex items-center justify-center"
|
||||||
|
onClick={() => handleReReview(reviewPoint.id, 'false')}
|
||||||
|
>
|
||||||
|
<i className="ri-refresh-line mr-1"></i> 重新审核
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 非通过状态,显示内容和修改建议
|
// 非通过状态,显示内容和修改建议
|
||||||
const isErrorStatus = reviewPoint.result === false && reviewPoint.status === 'error';
|
const isErrorStatus = reviewPoint.result === false && reviewPoint.status === 'error';
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
{reviewPoint.content !== null && (
|
{reviewPoint.content !== null && (
|
||||||
@@ -443,8 +517,18 @@ export function ReviewPointsList({
|
|||||||
(typeof reviewPoint.content === 'object' && Object.keys(reviewPoint.content).length > 0)
|
(typeof reviewPoint.content === 'object' && Object.keys(reviewPoint.content).length > 0)
|
||||||
) && (
|
) && (
|
||||||
<>
|
<>
|
||||||
|
{/* 建议内容显示区域 */}
|
||||||
|
{reviewPoint.suggestion && (
|
||||||
|
<div className="p-2 bg-blue-50 rounded border border-blue-200 text-xs mb-3 select-text">
|
||||||
|
<div className="flex items-start">
|
||||||
|
<i className="ri-information-line text-blue-500 mr-2 mt-0.5"></i>
|
||||||
|
<p className="text-xs text-gray-600 select-text">{reviewPoint.suggestion}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 内容显示区域 */}
|
{/* 内容显示区域 */}
|
||||||
<div className="p-2 bg-white rounded border border-gray-200 text-xs mb-3">
|
<div className="p-2 bg-white rounded border border-gray-200 text-xs mb-3 select-text">
|
||||||
{/* 移除顶部的"当前值"标题,在每个内容项中显示 */}
|
{/* 移除顶部的"当前值"标题,在每个内容项中显示 */}
|
||||||
{typeof reviewPoint.content === 'object' && reviewPoint.content !== null ? (
|
{typeof reviewPoint.content === 'object' && reviewPoint.content !== null ? (
|
||||||
// 当 content 是对象时的渲染方式
|
// 当 content 是对象时的渲染方式
|
||||||
@@ -461,7 +545,7 @@ export function ReviewPointsList({
|
|||||||
{value ? '' : '缺失'}
|
{value ? '' : '缺失'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-left">{value || ' '}</p>
|
<p className="text-xs text-left select-text">{value || (value === '' ? <span className="invisible">占位符</span> : '')}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -475,7 +559,7 @@ export function ReviewPointsList({
|
|||||||
{isErrorStatus ? '不符合规范' : '需优化'}
|
{isErrorStatus ? '不符合规范' : '需优化'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-left">{reviewPoint.content || '(内容为空)'}</p>
|
<p className="text-xs text-left select-text">{reviewPoint.content || '(内容为空)'}</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -484,22 +568,22 @@ export function ReviewPointsList({
|
|||||||
{reviewPoint.legalBasis && (typeof reviewPoint.legalBasis === 'object') && (
|
{reviewPoint.legalBasis && (typeof reviewPoint.legalBasis === 'object') && (
|
||||||
(reviewPoint.legalBasis.name || reviewPoint.legalBasis.content ||
|
(reviewPoint.legalBasis.name || reviewPoint.legalBasis.content ||
|
||||||
(reviewPoint.legalBasis.articles && Array.isArray(reviewPoint.legalBasis.articles) && reviewPoint.legalBasis.articles.length > 0)) && (
|
(reviewPoint.legalBasis.articles && Array.isArray(reviewPoint.legalBasis.articles) && reviewPoint.legalBasis.articles.length > 0)) && (
|
||||||
<div className="p-2 bg-white rounded border border-gray-200 text-xs mb-3">
|
<div className="p-2 bg-white rounded border border-gray-200 text-xs mb-3 select-text">
|
||||||
<div className="flex justify-between items-center mb-1">
|
<div className="flex justify-between items-center mb-1">
|
||||||
<span className="text-xs font-medium">法律依据</span>
|
<span className="text-xs font-medium">法律依据</span>
|
||||||
</div>
|
</div>
|
||||||
{reviewPoint.legalBasis.name && (
|
{reviewPoint.legalBasis.name && (
|
||||||
<p className="text-xs text-left mb-1">{reviewPoint.legalBasis.name}</p>
|
<p className="text-xs text-left mb-1 select-text">{reviewPoint.legalBasis.name}</p>
|
||||||
)}
|
)}
|
||||||
{reviewPoint.legalBasis.content && (
|
{reviewPoint.legalBasis.content && (
|
||||||
<p className="text-xs text-left mb-1"><span className="font-medium">条款内容:</span>{reviewPoint.legalBasis.content}</p>
|
<p className="text-xs text-left mb-1 select-text"><span className="font-medium">条款内容:</span>{reviewPoint.legalBasis.content}</p>
|
||||||
)}
|
)}
|
||||||
{reviewPoint.legalBasis.articles && Array.isArray(reviewPoint.legalBasis.articles) && reviewPoint.legalBasis.articles.length > 0 && (
|
{reviewPoint.legalBasis.articles && Array.isArray(reviewPoint.legalBasis.articles) && reviewPoint.legalBasis.articles.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-left font-medium mb-1">相关条款:</p>
|
<p className="text-xs text-left font-medium mb-1">相关条款:</p>
|
||||||
<ul className="list-disc pl-4">
|
<ul className="list-disc pl-4 select-text">
|
||||||
{reviewPoint.legalBasis.articles.map((item, index) => (
|
{reviewPoint.legalBasis.articles.map((item, index) => (
|
||||||
<li key={index} className="text-xs text-left">
|
<li key={index} className="text-xs text-left select-text">
|
||||||
{typeof item === 'string' ? item :
|
{typeof item === 'string' ? item :
|
||||||
typeof item === 'object' && item !== null ?
|
typeof item === 'object' && item !== null ?
|
||||||
(item.name ? `${item.name}: ${item.content || ''}` :
|
(item.name ? `${item.name}: ${item.content || ''}` :
|
||||||
@@ -515,22 +599,25 @@ export function ReviewPointsList({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 建议修改区域 */}
|
{/* 建议修改区域 */}
|
||||||
|
{/* {(reviewPoint.postAction !== 'none') && ( */}
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<div className="flex justify-between items-center mb-2">
|
<div className="flex justify-between items-center mb-2">
|
||||||
<span className="text-gray-700">建议修改为:</span>
|
<span className="text-gray-700 text-[0.8rem]">{reviewPoint.postAction === 'manual' ? "审核意见:" : "建议修改为:"}</span>
|
||||||
{/* <span className="text-green-500">符合规范</span> */}
|
{/* <span className="text-green-500">符合规范</span> */}
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
value={suggestionTexts[reviewPoint.id] || ''}
|
value={manualReviewNotes[reviewPoint.id] || ''}
|
||||||
onChange={(e) => handleSuggestionChange(reviewPoint.id, e.target.value)}
|
placeholder={reviewPoint.postAction === 'manual' ? "请输入审核意见(可选)..." : "请输入建议修改内容..."}
|
||||||
className="w-full p-2 border rounded bg-white min-h-[100px] focus:outline-none focus:border-[#00684a] focus:shadow-[0_0_0_2px_rgba(0,104,74,0.2)]"
|
onChange={(e) => handleManualReviewNotesChange(reviewPoint.id, e.target.value)}
|
||||||
|
className="text-xs w-full p-2 border rounded bg-white min-h-[100px] focus:outline-none focus:border-[#00684a] focus:shadow-[0_0_0_2px_rgba(0,104,74,0.2)]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{/* )} */}
|
||||||
|
|
||||||
{/* 操作按钮区域 */}
|
{/* 操作按钮区域 */}
|
||||||
<div className="flex space-x-2 mt-2">
|
<div className="flex space-x-2 mt-2">
|
||||||
{/* 一键替换按钮 - 只有非人工审核的点或未通过的人工审核点才显示 */}
|
{/* 一键替换按钮 - 只有非人工审核的点或未通过的人工审核点才显示 */}
|
||||||
{(!reviewPoint.needsHumanReview || (!reviewPoint.result && reviewPoint.status !== 'success')) && (
|
{(reviewPoint.postAction !== 'manual') && (
|
||||||
<button
|
<button
|
||||||
className="replace-action flex-1 justify-center"
|
className="replace-action flex-1 justify-center"
|
||||||
onClick={() => handleReplace(reviewPoint.id)}
|
onClick={() => handleReplace(reviewPoint.id)}
|
||||||
@@ -540,13 +627,27 @@ export function ReviewPointsList({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 人工审核按钮 */}
|
{/* 人工审核按钮 */}
|
||||||
{reviewPoint.needsHumanReview && !reviewPoint.result && reviewPoint.status !== 'success' && (
|
{reviewPoint.postAction === 'manual' && (
|
||||||
|
<div className="w-full flex justify-end gap-2">
|
||||||
<button
|
<button
|
||||||
className="replace-action flex-1 justify-center human-review-request"
|
className="bg-[#1890ff] hover:bg-blue-600 text-white py-1 px-2 rounded-md text-sm"
|
||||||
onClick={() => handleEditReviewPoint(reviewPoint.id)}
|
onClick={() => {
|
||||||
|
const note = manualReviewNotes[reviewPoint.id] || '';
|
||||||
|
handleReviewAction(reviewPoint.id, 'approve', note);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<i className="ri-edit-line"></i> 人工审核
|
<i className="ri-check-line mr-1"></i> 通过
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className="bg-[#f5222d] hover:bg-red-600 text-white py-1 px-2 rounded-md text-sm"
|
||||||
|
onClick={() => {
|
||||||
|
const note = manualReviewNotes[reviewPoint.id] || '';
|
||||||
|
handleReviewAction(reviewPoint.id, 'reject', note);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i className="ri-close-line mr-1"></i> 不通过
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -563,7 +664,7 @@ export function ReviewPointsList({
|
|||||||
return (
|
return (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
{/* 内容显示区域 */}
|
{/* 内容显示区域 */}
|
||||||
<div className="p-2 bg-white rounded border border-gray-200 text-xs mb-3">
|
<div className="p-2 bg-white rounded border border-gray-200 text-xs mb-3 select-text">
|
||||||
{/* 隐藏顶部的"当前值"标题,在每个内容项中显示 */}
|
{/* 隐藏顶部的"当前值"标题,在每个内容项中显示 */}
|
||||||
{typeof reviewPoint.content === 'object' && reviewPoint.content !== null ? (
|
{typeof reviewPoint.content === 'object' && reviewPoint.content !== null ? (
|
||||||
// 当 content 是对象时的渲染方式
|
// 当 content 是对象时的渲染方式
|
||||||
@@ -577,7 +678,7 @@ export function ReviewPointsList({
|
|||||||
{isErrorStatus ? '不符合规范' : '需优化'}
|
{isErrorStatus ? '不符合规范' : '需优化'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-left">{value || '(内容为空)'}</p>
|
<p className="text-xs text-left select-text">{value || (value === '' ? <span className="invisible">占位符</span> : '')}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -591,7 +692,7 @@ export function ReviewPointsList({
|
|||||||
{isErrorStatus ? '不符合规范' : '需优化'}
|
{isErrorStatus ? '不符合规范' : '需优化'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs">{reviewPoint.content || '(内容为空)'}</p>
|
<p className="text-xs select-text">{reviewPoint.content || '(内容为空)'}</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -599,8 +700,8 @@ export function ReviewPointsList({
|
|||||||
{/* 建议修改区域 */}
|
{/* 建议修改区域 */}
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<div className="flex justify-between items-center mb-2">
|
<div className="flex justify-between items-center mb-2">
|
||||||
<span className="text-gray-700">建议修改为:</span>
|
<span className="text-gray-700 text-xs">建议修改为:</span>
|
||||||
<span className="text-green-500">符合规范</span>
|
{/* <span className="text-green-500 text-xs">符合规范</span> */}
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
value={suggestionTexts[reviewPoint.id] || ''}
|
value={suggestionTexts[reviewPoint.id] || ''}
|
||||||
@@ -610,26 +711,26 @@ export function ReviewPointsList({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 审核意见区域 */}
|
{/* 审核意见区域 */}
|
||||||
<div className="bg-gray-50 rounded border border-gray-200 p-2">
|
<div className="bg-blue-50 rounded border border-blue-200 p-2">
|
||||||
<label htmlFor="reviewNote" className="block text-xs text-gray-700 mb-1">审核意见</label>
|
<label htmlFor="reviewNote" className="block text-xs text-gray-700 mb-1">审核意见</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="reviewNote"
|
id="reviewNote"
|
||||||
className="w-full border border-gray-200 rounded-md text-xs p-2 mb-2"
|
className="w-full border border-gray-200 rounded-md text-xs p-2 mb-2"
|
||||||
placeholder="请输入审核意见(可选)..."
|
placeholder="请输入审核意见(可选)..."
|
||||||
rows={2}
|
rows={2}
|
||||||
value={userInputText}
|
value={manualReviewNotes[reviewPoint.id] || ''}
|
||||||
onChange={(e) => setUserInputText(e.target.value)}
|
onChange={(e) => handleManualReviewNotesChange(reviewPoint.id, e.target.value)}
|
||||||
></textarea>
|
></textarea>
|
||||||
<div className="flex justify-end mt-2 space-x-2">
|
<div className="flex justify-end mt-2 space-x-2">
|
||||||
<button
|
<button
|
||||||
className="replace-action"
|
className="replace-action"
|
||||||
onClick={() => handleReviewAction(reviewPoint.id, 'approve')}
|
onClick={() => handleReviewAction(reviewPoint.id, 'approve', manualReviewNotes[reviewPoint.id] || '')}
|
||||||
>
|
>
|
||||||
<i className="ri-check-line"></i> 通过
|
<i className="ri-check-line"></i> 通过
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="replace-action status-error"
|
className="replace-action status-error"
|
||||||
onClick={() => handleReviewAction(reviewPoint.id, 'reject')}
|
onClick={() => handleReviewAction(reviewPoint.id, 'reject', manualReviewNotes[reviewPoint.id] || '')}
|
||||||
>
|
>
|
||||||
<i className="ri-close-line"></i> 不通过
|
<i className="ri-close-line"></i> 不通过
|
||||||
</button>
|
</button>
|
||||||
@@ -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 (
|
return (
|
||||||
<div className="review-points-panel">
|
<div className="review-points-panel select-text">
|
||||||
{/* 面板头部 */}
|
{/* 面板头部 */}
|
||||||
<div className="review-panel-header py-2 px-4 flex items-center">
|
<div className="review-panel-header py-2 px-4 flex items-center">
|
||||||
<i className="ri-file-list-check-line text-primary mr-2"></i>
|
<i className="ri-file-list-check-line text-primary mr-2"></i>
|
||||||
@@ -686,23 +800,23 @@ export function ReviewPointsList({
|
|||||||
<button
|
<button
|
||||||
key={reviewPoint.id}
|
key={reviewPoint.id}
|
||||||
className={`review-point-item ${activeReviewPointId === reviewPoint.id ? 'active' : ''}`}
|
className={`review-point-item ${activeReviewPointId === reviewPoint.id ? 'active' : ''}`}
|
||||||
onClick={() => onReviewPointSelect(reviewPoint.id)}
|
onClick={() => handleReviewPointClick(reviewPoint.id)}
|
||||||
type="button"
|
type="button"
|
||||||
|
style={{ userSelect: 'text' }}
|
||||||
>
|
>
|
||||||
{/* 评查点标题和状态 */}
|
{/* 评查点标题和状态 */}
|
||||||
<div className="review-point-header flex justify-between items-start">
|
<div className="review-point-header flex justify-between items-start">
|
||||||
<div className="review-point-title flex-1 text-left">{reviewPoint.title}</div>
|
<div className="review-point-title flex-1 text-left">{reviewPoint.title}</div>
|
||||||
<div className="flex items-center ml-2 flex-shrink-0">
|
|
||||||
{renderStatusBadge(reviewPoint.status, reviewPoint.result)}
|
|
||||||
{renderHumanReviewBadge(reviewPoint)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 评查点所属分组 */}
|
{/* 评查点所属分组 */}
|
||||||
<div className="review-point-location">
|
<div className="review-point-location">
|
||||||
<i className="ri-file-list-line mr-1"></i>
|
<i className="ri-file-list-line mr-1"></i>
|
||||||
<span>{reviewPoint.groupName}</span>
|
<span>{reviewPoint.groupName}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-col items-center ml-2 flex-shrink-0">
|
||||||
|
{renderStatusBadge(reviewPoint.status, reviewPoint.result)}
|
||||||
|
{renderHumanReviewBadge(reviewPoint)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 人工审核注释 */}
|
{/* 人工审核注释 */}
|
||||||
{renderHumanReviewNote(reviewPoint)}
|
{renderHumanReviewNote(reviewPoint)}
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ interface ReviewTabsProps {
|
|||||||
|
|
||||||
export function ReviewTabs({ activeTab, onTabChange, children }: ReviewTabsProps) {
|
export function ReviewTabs({ activeTab, onTabChange, children }: ReviewTabsProps) {
|
||||||
return (
|
return (
|
||||||
<div className="tab-container">
|
<div className="tab-container w-full flex-1">
|
||||||
<div className="tab-nav">
|
<div className="tab-nav w-full flex">
|
||||||
<button
|
<button
|
||||||
className={`tab-nav-item ${activeTab === 'preview' ? 'active' : ''}`}
|
className={`tab-nav-item ${activeTab === 'preview' ? 'active' : ''}`}
|
||||||
onClick={() => onTabChange('preview')}
|
onClick={() => onTabChange('preview')}
|
||||||
@@ -40,7 +40,7 @@ export function ReviewTabs({ activeTab, onTabChange, children }: ReviewTabsProps
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="tab-content">
|
<div className="tab-content w-full">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+46
-8
@@ -8,6 +8,8 @@ import { FileTag, links as fileTagLinks } from "~/components/ui/FileTag";
|
|||||||
import { Tag } from "~/components/ui/Tag";
|
import { Tag } from "~/components/ui/Tag";
|
||||||
import homeStyles from "~/styles/pages/home.css?url";
|
import homeStyles from "~/styles/pages/home.css?url";
|
||||||
import { getDocuments, type DocumentUI } from "~/api/files/documents";
|
import { getDocuments, type DocumentUI } from "~/api/files/documents";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
// 文件处理状态选项
|
// 文件处理状态选项
|
||||||
const fileProcessingStatusOptions = [
|
const fileProcessingStatusOptions = [
|
||||||
@@ -87,33 +89,69 @@ export async function loader() {
|
|||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const { stats, recentFiles } = useLoaderData<typeof loader>();
|
const { stats, recentFiles } = useLoaderData<typeof loader>();
|
||||||
|
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 (
|
return (
|
||||||
<div className="dashboard-container">
|
<div className="dashboard-container">
|
||||||
{/* 页面标识 */}
|
{/* 页面头部 */}
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h2 className="text-xl font-medium">系统概览</h2>
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
<span id="current-date">{currentDateTime.date}</span>
|
||||||
|
<span className="mx-2">|</span>
|
||||||
|
<span id="current-time">{currentDateTime.time}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 统计卡片区域 */}
|
{/* 统计卡片区域 */}
|
||||||
<Card title="统计信息" icon="ri-bar-chart-line" className="mt-6 transition-all duration-200 hover:shadow-[0_4px_15px_rgba(0,0,0,0.1)]">
|
<Card title="统计信息" icon="ri-bar-chart-line" className="mt-6 transition-all duration-200 hover:shadow-[0_4px_15px_rgba(0,0,0,0.1)]">
|
||||||
<div className="stat-grid ">
|
<div className="stat-grid ">
|
||||||
<StatCard
|
<StatCard
|
||||||
title="总文件数"
|
title="今日待审文件"
|
||||||
value={stats.totalFiles}
|
value={stats.totalFiles}
|
||||||
icon="ri-file-list-3-line"
|
icon="ri-file-list-3-line"
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
title="已审核"
|
title="本月已审核文件"
|
||||||
value={stats.reviewedFiles}
|
value={stats.reviewedFiles}
|
||||||
icon="ri-check-double-line"
|
icon="ri-check-double-line"
|
||||||
trend={{ value: 5.2, isUp: true }}
|
trend={{ value: 5.2, isUp: true }}
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
title="待审核"
|
title="审核通过率"
|
||||||
value={stats.pendingFiles}
|
value={stats.pendingFiles}
|
||||||
icon="ri-time-line"
|
icon="ri-time-line"
|
||||||
trend={{ value: 2.1, isUp: false }}
|
trend={{ value: 2.1, isUp: false }}
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
title="通过率"
|
title="问题检出数"
|
||||||
value={`${stats.passRate}%`}
|
value={`${stats.passRate}%`}
|
||||||
icon="ri-pie-chart-line"
|
icon="ri-pie-chart-line"
|
||||||
trend={{ value: 1.5, isUp: true }}
|
trend={{ value: 1.5, isUp: true }}
|
||||||
@@ -125,10 +163,10 @@ export default function Index() {
|
|||||||
<Card title="快捷访问" icon="ri-speed-line" className="mt-6 transition-all duration-200 hover:shadow-[0_4px_15px_rgba(0,0,0,0.1)]">
|
<Card title="快捷访问" icon="ri-speed-line" className="mt-6 transition-all duration-200 hover:shadow-[0_4px_15px_rgba(0,0,0,0.1)]">
|
||||||
<div className="shortcut-grid">
|
<div className="shortcut-grid">
|
||||||
<ShortcutItem icon="ri-upload-cloud-line" label="上传文件" to="/files/upload" />
|
<ShortcutItem icon="ri-upload-cloud-line" label="上传文件" to="/files/upload" />
|
||||||
<ShortcutItem icon="ri-file-list-3-line" label="文件列表" to="/documents" />
|
<ShortcutItem icon="ri-file-list-3-line" label="文档列表" to="/documents" />
|
||||||
<ShortcutItem icon="ri-list-check-2" label="评查点管理" to="/rules" />
|
<ShortcutItem icon="ri-list-check-3" label="评查点列表" to="/rules" />
|
||||||
<ShortcutItem icon="ri-folder-open-line" label="评查点分组" to="/rule-groups" />
|
<ShortcutItem icon="ri-folder-open-line" label="评查点分组" to="/rule-groups" />
|
||||||
<ShortcutItem icon="ri-file-chart-line" label="评查详情" to="/reviews" />
|
{/* <ShortcutItem icon="ri-file-chart-line" label="评查详情" to="/reviews" /> */}
|
||||||
<ShortcutItem icon="ri-file-list-line" label="文档类型" to="/document-types" />
|
<ShortcutItem icon="ri-file-list-line" label="文档类型" to="/document-types" />
|
||||||
{/* <ShortcutItem icon="ri-settings-3-line" label="系统设置" to="/settings" /> */}
|
{/* <ShortcutItem icon="ri-settings-3-line" label="系统设置" to="/settings" /> */}
|
||||||
<ShortcutItem icon="ri-chat-1-line" label="提示词管理" to="/prompts" />
|
<ShortcutItem icon="ri-chat-1-line" label="提示词管理" to="/prompts" />
|
||||||
|
|||||||
+156
-27
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from "react";
|
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 { type MetaFunction, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node";
|
||||||
import { Card } from "~/components/ui/Card";
|
import { Card } from "~/components/ui/Card";
|
||||||
import { Button } from "~/components/ui/Button";
|
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 documentsIndexStyles from "~/styles/pages/documents_index.css?url";
|
||||||
import { getDocuments, deleteDocument, type DocumentUI } from "~/api/files/documents";
|
import { getDocuments, deleteDocument, type DocumentUI } from "~/api/files/documents";
|
||||||
import { getDocumentTypes } from "~/api/document-types/document-types";
|
import { getDocumentTypes } from "~/api/document-types/document-types";
|
||||||
|
import { updateDocumentAuditStatus } from "~/api/evaluation_points/rules-files";
|
||||||
|
|
||||||
// 导入样式
|
// 导入样式
|
||||||
export function links() {
|
export function links() {
|
||||||
@@ -119,11 +120,11 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
|||||||
// 审核状态筛选选项
|
// 审核状态筛选选项
|
||||||
const auditStatusOptions = [
|
const auditStatusOptions = [
|
||||||
// { value: "", label: "全部" },
|
// { value: "", label: "全部" },
|
||||||
|
{ value: "-2", label: "警告" },
|
||||||
{ value: "-1", label: "不通过" },
|
{ value: "-1", label: "不通过" },
|
||||||
{ value: "0", label: "待审核" },
|
{ value: "0", label: "待审核" },
|
||||||
{ value: "1", label: "通过" },
|
{ value: "1", label: "通过" },
|
||||||
{ value: "2", label: "警告" },
|
{ value: "2", label: "审核中" },
|
||||||
{ value: "3", label: "审核中" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// 文件处理状态选项
|
// 文件处理状态选项
|
||||||
@@ -148,10 +149,10 @@ const fileStatusOptions = [
|
|||||||
// 审核状态选项及样式
|
// 审核状态选项及样式
|
||||||
const auditStatusMapping: Record<string, { label: string; color: string; icon: string }> = {
|
const auditStatusMapping: Record<string, { label: string; color: string; icon: string }> = {
|
||||||
"-1": { label: "不通过", color: "red", icon: "ri-close-line" },
|
"-1": { label: "不通过", color: "red", icon: "ri-close-line" },
|
||||||
|
"-2": { label: "警告", color: "yellow", icon: "ri-alert-line" },
|
||||||
"0": { label: "待审核", color: "blue", icon: "ri-time-line" },
|
"0": { label: "待审核", color: "blue", icon: "ri-time-line" },
|
||||||
"1": { label: "通过", color: "green", icon: "ri-check-line" },
|
"1": { label: "通过", color: "green", icon: "ri-check-line" },
|
||||||
"2": { label: "警告", color: "yellow", icon: "ri-alert-line" },
|
"2": { label: "审核中", color: "purple", icon: "ri-search-line" },
|
||||||
"3": { label: "审核中", color: "purple", icon: "ri-search-line" },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 格式化文件大小
|
// 格式化文件大小
|
||||||
@@ -182,6 +183,7 @@ export default function DocumentsIndex() {
|
|||||||
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
|
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
|
||||||
const loaderData = useLoaderData<typeof loader>();
|
const loaderData = useLoaderData<typeof loader>();
|
||||||
const fetcher = useFetcher();
|
const fetcher = useFetcher();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// 从URL获取当前筛选条件
|
// 从URL获取当前筛选条件
|
||||||
const search = searchParams.get("search") || "";
|
const search = searchParams.get("search") || "";
|
||||||
@@ -321,29 +323,41 @@ export default function DocumentsIndex() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 下载文档
|
// 下载文档
|
||||||
const handleDownload = async (path: string, fileName: string) => {
|
const handleDownload = async (path: string) => {
|
||||||
console.log('handleDownload',path,fileName)
|
|
||||||
try {
|
try {
|
||||||
// 使用API获取授权的下载链接
|
const urlBefore = 'http://172.18.0.100:9000/docauditai/';
|
||||||
// const { data, error } = await getFileDownloadUrl(path);
|
const downloadUrl = `${urlBefore}${path}`;
|
||||||
|
|
||||||
// if (error || !data?.downloadUrl) {
|
// 使用fetch获取文件内容
|
||||||
// console.error('获取下载链接失败:', error);
|
const response = await fetch(downloadUrl);
|
||||||
// alert('获取下载链接失败: ' + (error || '未知错误'));
|
if (!response.ok) {
|
||||||
// return;
|
throw new Error(`下载失败: ${response.status} ${response.statusText}`);
|
||||||
// }
|
}
|
||||||
|
|
||||||
|
// 将响应转换为Blob
|
||||||
|
const blob = await response.blob();
|
||||||
|
|
||||||
|
// 创建Blob URL
|
||||||
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
// 创建一个隐藏的a标签并点击它
|
// 创建一个隐藏的a标签并点击它
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
// a.href = data.downloadUrl;
|
a.style.display = 'none';
|
||||||
a.href = path;
|
a.href = blobUrl;
|
||||||
a.download = fileName; // 设置下载的文件名
|
// 从路径中获取文件名
|
||||||
|
const fileName = path.split('/').pop() || 'document';
|
||||||
|
a.download = decodeURIComponent(fileName);
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
setTimeout(() => {
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
} catch (err) {
|
URL.revokeObjectURL(blobUrl);
|
||||||
console.error('下载文件失败:', err);
|
}, 100);
|
||||||
alert('下载文件失败: ' + (err instanceof Error ? err.message : '未知错误'));
|
} catch (error) {
|
||||||
|
console.error('下载文件失败:', error);
|
||||||
|
alert(`下载文件失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -412,6 +426,114 @@ export default function DocumentsIndex() {
|
|||||||
setSearchParams(params);
|
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 = [
|
const columns = [
|
||||||
{
|
{
|
||||||
@@ -517,7 +639,7 @@ export default function DocumentsIndex() {
|
|||||||
{
|
{
|
||||||
title: "问题数量",
|
title: "问题数量",
|
||||||
key: "issues",
|
key: "issues",
|
||||||
width:"5%",
|
width:"7%",
|
||||||
render: (_: unknown, record: DocumentUI) => (
|
render: (_: unknown, record: DocumentUI) => (
|
||||||
record.issues === null ? "-" : record.issues
|
record.issues === null ? "-" : record.issues
|
||||||
)
|
)
|
||||||
@@ -535,14 +657,20 @@ export default function DocumentsIndex() {
|
|||||||
render: (_: unknown, record: DocumentUI) => (
|
render: (_: unknown, record: DocumentUI) => (
|
||||||
<div className="operations-cell">
|
<div className="operations-cell">
|
||||||
{(record.auditStatus === 0 || record.auditStatus == null) ? (
|
{(record.auditStatus === 0 || record.auditStatus == null) ? (
|
||||||
<Link
|
<button
|
||||||
to={`/reviews?id=${record.id}`}
|
onClick={() => handleReviewFileClick(record.id, record.auditStatus)}
|
||||||
className="mr-1 hover:underline"
|
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'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<i className="ri-play-circle-line"></i>
|
<i className="ri-play-circle-line"></i>
|
||||||
开始审核
|
开始审核
|
||||||
</Link>
|
</button>
|
||||||
) : record.auditStatus === 3 ? (
|
) : record.auditStatus === 3 ? (
|
||||||
|
//record.auditStatus === 3 目前这个状态不存在,所以除了待审核(0)-开始审核,其他都是审核中(2)-查看
|
||||||
<Link
|
<Link
|
||||||
to={`/documents/${record.id}/progress`}
|
to={`/documents/${record.id}/progress`}
|
||||||
className="mr-1 hover:underline"
|
className="mr-1 hover:underline"
|
||||||
@@ -552,7 +680,7 @@ export default function DocumentsIndex() {
|
|||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<Link
|
<Link
|
||||||
to={`/documents/${record.id}`}
|
to={`/reviews?id=${record.id}`}
|
||||||
className="mr-1 hover:underline"
|
className="mr-1 hover:underline"
|
||||||
>
|
>
|
||||||
<i className="ri-eye-line"></i>
|
<i className="ri-eye-line"></i>
|
||||||
@@ -569,7 +697,7 @@ export default function DocumentsIndex() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="mr-1 text-gray-500 hover:underline hover:text-gray-700"
|
className="mr-1 text-gray-500 hover:underline hover:text-gray-700"
|
||||||
onClick={() => handleDownload(record.path, record.name)}
|
onClick={() => handleDownload(record.path)}
|
||||||
>
|
>
|
||||||
<i className="ri-download-line"></i>
|
<i className="ri-download-line"></i>
|
||||||
下载
|
下载
|
||||||
@@ -699,6 +827,7 @@ export default function DocumentsIndex() {
|
|||||||
<Button
|
<Button
|
||||||
type="default"
|
type="default"
|
||||||
icon="ri-download-line"
|
icon="ri-download-line"
|
||||||
|
onClick={handleExport}
|
||||||
>
|
>
|
||||||
导出列表
|
导出列表
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -198,9 +198,9 @@ export default function DocumentEdit() {
|
|||||||
// 在新窗口打开文档预览
|
// 在新窗口打开文档预览
|
||||||
const openPreview = () => {
|
const openPreview = () => {
|
||||||
// 假设有一个预览URL的格式,比如 /preview?path=xxx
|
// 假设有一个预览URL的格式,比如 /preview?path=xxx
|
||||||
console.log('documentstest', document);
|
// console.log('documentstest', document);
|
||||||
|
const urlBefore = 'http://172.18.0.100:9000/docauditai/'
|
||||||
const previewUrl = `/preview?path=${encodeURIComponent(document.path)}&name=${encodeURIComponent(document.name)}`;
|
const previewUrl = `${urlBefore}${document.path}`;
|
||||||
window.open(previewUrl, '_blank');
|
window.open(previewUrl, '_blank');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+129
-36
@@ -28,9 +28,8 @@
|
|||||||
import { type MetaFunction, type LoaderFunctionArgs } from "@remix-run/node";
|
import { type MetaFunction, type LoaderFunctionArgs } from "@remix-run/node";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useNavigate, useLoaderData } from "@remix-run/react";
|
import { useNavigate, useLoaderData } from "@remix-run/react";
|
||||||
import {getDocument} from "~/api/files/documents";
|
|
||||||
import reviewsStyles from "~/styles/reviews.css?url";
|
import reviewsStyles from "~/styles/reviews.css?url";
|
||||||
import { getReviewPoints } from "~/api/evaluation_points/reviews";
|
import { getReviewPoints, updateReviewResult } from "~/api/evaluation_points/reviews";
|
||||||
|
|
||||||
// 导入评查详情页面组件
|
// 导入评查详情页面组件
|
||||||
import {
|
import {
|
||||||
@@ -176,25 +175,28 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
return Response.json({ error: '评查ID不能为空' }, { status: 400 });
|
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);
|
const reviewData = await getReviewPoints(id);
|
||||||
|
|
||||||
console.log("reviewData-------",JSON.stringify(reviewData.data,null,2));
|
// console.log("documentData-------",JSON.stringify(documentData.data,null,2));
|
||||||
if (reviewData.error) {
|
console.log("reviewData-------",JSON.stringify('data' in reviewData ? reviewData.data : '',null,2));
|
||||||
|
if ('error' in reviewData && reviewData.error) {
|
||||||
console.error("获取评查点数据错误:", reviewData.error);
|
console.error("获取评查点数据错误:", reviewData.error);
|
||||||
return Response.json({ error: reviewData.error }, { status: reviewData.status || 500 });
|
return Response.json({ error: reviewData.error }, { status: reviewData.status || 500 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 确保reviewData有效且具有预期的属性
|
||||||
|
if ('document' in reviewData && 'data' in reviewData && 'reviewInfo' in reviewData && 'stats' in reviewData) {
|
||||||
return Response.json({
|
return Response.json({
|
||||||
document: documentData.data,
|
document: reviewData.document,
|
||||||
reviewPoints: reviewData.data,
|
reviewPoints: reviewData.data,
|
||||||
reviewInfo: reviewData.reviewInfo,
|
reviewInfo: reviewData.reviewInfo,
|
||||||
statistics: reviewData.stats
|
statistics: reviewData.stats
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
console.error("返回的评查数据格式不正确");
|
||||||
|
return Response.json({ error: '返回的评查数据格式不正确' }, { status: 500 });
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取评查数据失败:', error);
|
console.error('获取评查数据失败:', error);
|
||||||
return Response.json({ error: '获取评查数据失败' }, { status: 500 });
|
return Response.json({ error: '获取评查数据失败' }, { status: 500 });
|
||||||
@@ -208,6 +210,7 @@ export default function ReviewDetails() {
|
|||||||
const [activeTab, setActiveTab] = useState<string>('preview'); // 'preview', 'analysis', 'fileinfo'
|
const [activeTab, setActiveTab] = useState<string>('preview'); // 'preview', 'analysis', 'fileinfo'
|
||||||
const [reviewData, setReviewData] = useState<ReviewData | null>(null);
|
const [reviewData, setReviewData] = useState<ReviewData | null>(null);
|
||||||
const [activeReviewPointId, setActiveReviewPointId] = useState<string | null>(null);
|
const [activeReviewPointId, setActiveReviewPointId] = useState<string | null>(null);
|
||||||
|
const [targetPage, setTargetPage] = useState<number | undefined>(undefined);
|
||||||
|
|
||||||
// 模拟获取评查数据
|
// 模拟获取评查数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -238,7 +241,7 @@ export default function ReviewDetails() {
|
|||||||
reviewPoints: reviewPoints,
|
reviewPoints: reviewPoints,
|
||||||
aiAnalysis: getMockReviewData().aiAnalysis,
|
aiAnalysis: getMockReviewData().aiAnalysis,
|
||||||
};
|
};
|
||||||
console.log("reviewDataObj-------",reviewDataObj);
|
|
||||||
|
|
||||||
setReviewData(reviewDataObj);
|
setReviewData(reviewDataObj);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -248,22 +251,130 @@ export default function ReviewDetails() {
|
|||||||
setActiveTab(tabKey);
|
setActiveTab(tabKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReviewPointSelect = (reviewPointId: string) => {
|
const handleReviewPointSelect = (reviewPointId: string, page?: number) => {
|
||||||
setActiveReviewPointId(reviewPointId);
|
setActiveReviewPointId(reviewPointId);
|
||||||
|
setTargetPage(page);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReviewPointStatusChange = (reviewPointId: string, newStatus: string) => {
|
// 刷新评审数据
|
||||||
// 更新评查点状态
|
async function refreshReviewData(documentId: string) {
|
||||||
|
// 设置加载状态
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
// 获取最新的评审数据
|
||||||
|
const response = await getReviewPoints(documentId);
|
||||||
|
|
||||||
|
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) {
|
if (reviewData) {
|
||||||
const updatedReviewPoints = reviewData.reviewPoints.map(point =>
|
const updatedReviewPoints = reviewData.reviewPoints.map(point =>
|
||||||
point.id === reviewPointId ? { ...point, status: newStatus } : point
|
point.id === reviewPointResultId ? {
|
||||||
|
...point,
|
||||||
|
result: boolResult,
|
||||||
|
status: boolResult ? 'success' : 'error',
|
||||||
|
message: message
|
||||||
|
} : point
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 更新 UI 状态
|
||||||
setReviewData({
|
setReviewData({
|
||||||
...reviewData,
|
...reviewData,
|
||||||
reviewPoints: updatedReviewPoints,
|
reviewPoints: updatedReviewPoints,
|
||||||
statistics: calculateStatistics(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() {
|
|||||||
{/* 左侧:文件预览 */}
|
{/* 左侧:文件预览 */}
|
||||||
<div className="w-full lg:w-2/3">
|
<div className="w-full lg:w-2/3">
|
||||||
<FilePreview
|
<FilePreview
|
||||||
fileContent={reviewData.fileContent}
|
fileContent={document}
|
||||||
reviewPoints={reviewData.reviewPoints}
|
reviewPoints={reviewData.reviewPoints}
|
||||||
activeReviewPointId={activeReviewPointId}
|
activeReviewPointId={activeReviewPointId}
|
||||||
|
targetPage={targetPage}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -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 {
|
function getMockReviewData(): ReviewData {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -443,7 +443,7 @@ export default function RuleGroupsIndex() {
|
|||||||
{
|
{
|
||||||
title: "分组名称",
|
title: "分组名称",
|
||||||
key: "name",
|
key: "name",
|
||||||
width: "400px",
|
width: "35%",
|
||||||
render: (_: unknown, record: RuleGroup & { isParent?: boolean, parentId?: string }) => (
|
render: (_: unknown, record: RuleGroup & { isParent?: boolean, parentId?: string }) => (
|
||||||
<div className={`flex items-center ${!record.isParent ? 'ml-8' : ''}`}>
|
<div className={`flex items-center ${!record.isParent ? 'ml-8' : ''}`}>
|
||||||
{record.isParent && (
|
{record.isParent && (
|
||||||
@@ -467,7 +467,7 @@ export default function RuleGroupsIndex() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<Link
|
<Link
|
||||||
to={`/rule-groups/${record.id}/rules`}
|
to={`/rule-groups/new?id=${record.id}`}
|
||||||
className="group-name-link flex items-center ml-1 text-green-800"
|
className="group-name-link flex items-center ml-1 text-green-800"
|
||||||
>
|
>
|
||||||
<i className={`${record.isParent ? 'ri-folder-line' : 'ri-file-list-line'} mr-1 text-green-800`}></i> {record.name}
|
<i className={`${record.isParent ? 'ri-folder-line' : 'ri-file-list-line'} mr-1 text-green-800`}></i> {record.name}
|
||||||
@@ -488,10 +488,11 @@ export default function RuleGroupsIndex() {
|
|||||||
{
|
{
|
||||||
title: "评查点数量",
|
title: "评查点数量",
|
||||||
key: "ruleCount",
|
key: "ruleCount",
|
||||||
|
width: "12%",
|
||||||
render: (_: unknown, record: RuleGroup & { isParent?: boolean, parentId?: string }) => (
|
render: (_: unknown, record: RuleGroup & { isParent?: boolean, parentId?: string }) => (
|
||||||
<Link to={`/rule-groups/${record.id}/rules`} className="badge bg-primary text-white">
|
<button onClick={() => navigate(`/rules?${!record.isParent ? `ruleType=${record.parentId}&groupId=${record.id}` : `ruleType=${record.id}`}`)} className="badge bg-primary text-white">
|
||||||
{calculateTotalRuleCount(record)}
|
<span className="text-xs hover:underline">{calculateTotalRuleCount(record)}</span>
|
||||||
</Link>
|
</button>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -508,6 +509,7 @@ export default function RuleGroupsIndex() {
|
|||||||
{
|
{
|
||||||
title: "创建时间",
|
title: "创建时间",
|
||||||
key: "createdAt",
|
key: "createdAt",
|
||||||
|
width: "15%",
|
||||||
render: (_: unknown, record: RuleGroup) => (
|
render: (_: unknown, record: RuleGroup) => (
|
||||||
<span>{record.createdAt || '-'}</span>
|
<span>{record.createdAt || '-'}</span>
|
||||||
)
|
)
|
||||||
|
|||||||
+112
-11
@@ -1,5 +1,5 @@
|
|||||||
import { type MetaFunction, type LoaderFunctionArgs } from "@remix-run/node";
|
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 { Button } from "~/components/ui/Button";
|
||||||
import { Card } from "~/components/ui/Card";
|
import { Card } from "~/components/ui/Card";
|
||||||
import { FileIcon } from "~/components/ui/FileIcon";
|
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 rulesFilesStyles from "~/styles/pages/rules-files.css?url";
|
||||||
import {
|
import {
|
||||||
getReviewFiles,
|
getReviewFiles,
|
||||||
updateReviewStatus,
|
type ReviewFileUI,
|
||||||
type ReviewFileUI
|
updateDocumentAuditStatus
|
||||||
} from "~/api/evaluation_points/rules-files";
|
} from "~/api/evaluation_points/rules-files";
|
||||||
import { getDocumentTypes } from "~/api/document-types/document-types";
|
import { getDocumentTypes } from "~/api/document-types/document-types";
|
||||||
|
|
||||||
@@ -115,6 +115,7 @@ export function ErrorBoundary() {
|
|||||||
export default function RulesFiles() {
|
export default function RulesFiles() {
|
||||||
const { files, documentTypes, totalCount, currentPage, pageSize } = useLoaderData<typeof loader>();
|
const { files, documentTypes, totalCount, currentPage, pageSize } = useLoaderData<typeof loader>();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// 处理筛选条件变更
|
// 处理筛选条件变更
|
||||||
const handleFilterChange = (e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>) => {
|
const handleFilterChange = (e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>) => {
|
||||||
@@ -163,9 +164,30 @@ export default function RulesFiles() {
|
|||||||
setSearchParams(newParams);
|
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) => {
|
const renderIssues = (file: ReviewFileUI) => {
|
||||||
// 如果评查状态为通过,显示"所有评查点均通过"
|
// 如果评查状态为通过(说明所有评查结果为true),显示"所有评查点均通过"
|
||||||
|
if (file.status === 'Processed') {
|
||||||
if (file.reviewStatus === 'pass') {
|
if (file.reviewStatus === 'pass') {
|
||||||
return (
|
return (
|
||||||
<div className="text-sm text-success">
|
<div className="text-sm text-success">
|
||||||
@@ -174,10 +196,82 @@ export default function RulesFiles() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果评查状态为通过,显示"所有评查点均通过"
|
||||||
|
if (file.reviewStatus === 'fail') {
|
||||||
|
return (
|
||||||
|
<div className="text-sm text-error">
|
||||||
|
<i className="ri-error-warning-line mr-1"></i>统计分数为:{file.score || 0}。分数低于80分。
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示问题列表
|
||||||
|
if (file.reviewStatus !== 'pass' && file.reviewStatus !== 'fail' && file.issues && file.issues.length > 0) {
|
||||||
|
// 最多显示2个问题
|
||||||
|
const displayIssues = file.issues.slice(0, 2);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="text-sm">
|
||||||
|
{displayIssues.map((issue, index) => (
|
||||||
|
<div key={index} className="mb-1">
|
||||||
|
<i className="ri-circle-fill mr-1 text-warning"></i>
|
||||||
|
{issue.message}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{file.issues.length > 2 && (
|
||||||
|
<div className="text-secondary mt-1">
|
||||||
|
还有 {file.issues.length - 2} 个问题...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
// 其他状态显示占位符
|
// 其他状态显示占位符
|
||||||
return <div className="text-sm text-secondary">-</div>;
|
return <div className="text-sm text-secondary">-</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// 下载文件
|
||||||
|
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}) => ({
|
const fileTypeOptions = documentTypes.map((type: {id: number, name: string}) => ({
|
||||||
value: type.id.toString(),
|
value: type.id.toString(),
|
||||||
@@ -197,7 +291,7 @@ export default function RulesFiles() {
|
|||||||
{ value: DateRange.TODAY, label: '今天' },
|
{ value: DateRange.TODAY, label: '今天' },
|
||||||
{ value: DateRange.WEEK, label: '本周' },
|
{ value: DateRange.WEEK, label: '本周' },
|
||||||
{ value: DateRange.MONTH, label: '本月' },
|
{ value: DateRange.MONTH, label: '本月' },
|
||||||
{ value: DateRange.CUSTOM, label: '自定义时间段' }
|
// { value: DateRange.CUSTOM, label: '自定义时间段' }
|
||||||
];
|
];
|
||||||
|
|
||||||
// 定义表格列配置
|
// 定义表格列配置
|
||||||
@@ -252,12 +346,17 @@ export default function RulesFiles() {
|
|||||||
title: "评查状态",
|
title: "评查状态",
|
||||||
key: "reviewStatus",
|
key: "reviewStatus",
|
||||||
width: "12%",
|
width: "12%",
|
||||||
render: (_: unknown, file: ReviewFileUI) => (
|
render: (_: unknown, file: ReviewFileUI) =>
|
||||||
|
file.status === 'Processed' ? (
|
||||||
<StatusBadge
|
<StatusBadge
|
||||||
status={file.reviewStatus}
|
status={file.reviewStatus}
|
||||||
text={REVIEW_STATUS_LABELS[file.reviewStatus]}
|
text={`${REVIEW_STATUS_LABELS[file.reviewStatus]}${file.issueCount>0?'('+file.issueCount+')':''}`}
|
||||||
showIcon={true}
|
showIcon={true}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="text-sm">
|
||||||
|
-
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -276,13 +375,15 @@ export default function RulesFiles() {
|
|||||||
type="default"
|
type="default"
|
||||||
size="small"
|
size="small"
|
||||||
icon="ri-eye-line"
|
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"
|
className="mr-2"
|
||||||
>
|
>
|
||||||
查看
|
查看
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button type="default" size="small" icon="ri-download-2-line">
|
<Button type="default" size="small" icon="ri-download-2-line" className="mt-1" onClick={() => handleDownload(file.path)}>
|
||||||
下载
|
下载
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
@@ -344,8 +445,8 @@ export default function RulesFiles() {
|
|||||||
options={[
|
options={[
|
||||||
{ value: "upload_time_desc", label: "上传时间 ↓" },
|
{ value: "upload_time_desc", label: "上传时间 ↓" },
|
||||||
{ value: "upload_time_asc", label: "上传时间 ↑" },
|
{ value: "upload_time_asc", label: "上传时间 ↑" },
|
||||||
{ value: "issue_count_desc", label: "问题数量 ↓" },
|
// { value: "issue_count_desc", label: "问题数量 ↓" },
|
||||||
{ value: "issue_count_asc", label: "问题数量 ↑" }
|
// { value: "issue_count_asc", label: "问题数量 ↑" }
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<SearchFilter
|
<SearchFilter
|
||||||
|
|||||||
@@ -485,7 +485,7 @@ export default function RulesIndex() {
|
|||||||
}))
|
}))
|
||||||
]}
|
]}
|
||||||
onChange={handleFilterChange}
|
onChange={handleFilterChange}
|
||||||
className="mr-3 w-60 "
|
className="mr-3 w-[20%]"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FilterSelect
|
<FilterSelect
|
||||||
@@ -500,7 +500,7 @@ export default function RulesIndex() {
|
|||||||
}))
|
}))
|
||||||
]}
|
]}
|
||||||
onChange={handleFilterChange}
|
onChange={handleFilterChange}
|
||||||
className={`mr-3 w-60 ${isRuleGroupSelectDisabled ? 'opacity-50' : ''}`}
|
className={`mr-3 w-[20%] ${isRuleGroupSelectDisabled ? 'opacity-50' : ''}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FilterSelect
|
<FilterSelect
|
||||||
@@ -512,7 +512,7 @@ export default function RulesIndex() {
|
|||||||
{ value: "false", label: "禁用" }
|
{ value: "false", label: "禁用" }
|
||||||
]}
|
]}
|
||||||
onChange={handleFilterChange}
|
onChange={handleFilterChange}
|
||||||
className="mr-3 w-60"
|
className="mr-3 w-[20%]"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SearchFilter
|
<SearchFilter
|
||||||
@@ -520,7 +520,7 @@ export default function RulesIndex() {
|
|||||||
placeholder="输入评查点名称或编码"
|
placeholder="输入评查点名称或编码"
|
||||||
value={searchParams.get('keyword') || ''}
|
value={searchParams.get('keyword') || ''}
|
||||||
onSearch={handleSearch}
|
onSearch={handleSearch}
|
||||||
className="flex-1"
|
className="w-[30%]"
|
||||||
/>
|
/>
|
||||||
</FilterPanel>
|
</FilterPanel>
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
/* 仪表盘容器 */
|
/* 仪表盘容器 */
|
||||||
.dashboard-container {
|
.dashboard-container {
|
||||||
@apply p-5;
|
@apply p-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 统计卡片 */
|
/* 统计卡片 */
|
||||||
|
|||||||
@@ -11,6 +11,10 @@
|
|||||||
--bg-gray: #f5f5f5;
|
--bg-gray: #f5f5f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
/* 文件信息和操作按钮区域 */
|
/* 文件信息和操作按钮区域 */
|
||||||
.file-info-header {
|
.file-info-header {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
@@ -392,3 +396,15 @@
|
|||||||
.info-value {
|
.info-value {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-success {
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -4,8 +4,10 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>中国烟草AI合同及卷宗审核系统 - 首页</title>
|
<title>中国烟草AI合同及卷宗审核系统 - 首页</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css" rel="stylesheet">
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/remixicon/2.5.0/remixicon.css" rel="stylesheet">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet">
|
||||||
|
<!-- <link href="https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css" rel="stylesheet">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet"> -->
|
||||||
<!-- 引入外部CSS文件 -->
|
<!-- 引入外部CSS文件 -->
|
||||||
<link href="../css/main.css" rel="stylesheet">
|
<link href="../css/main.css" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
Generated
+4
-1
@@ -20,6 +20,7 @@
|
|||||||
"docx-preview": "^0.3.5",
|
"docx-preview": "^0.3.5",
|
||||||
"html-docx-js": "^0.3.1",
|
"html-docx-js": "^0.3.1",
|
||||||
"isbot": "^4.1.0",
|
"isbot": "^4.1.0",
|
||||||
|
"jszip": "^3.10.1",
|
||||||
"mammoth": "^1.9.0",
|
"mammoth": "^1.9.0",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
"pdfjs-dist": "^3.11.174",
|
"pdfjs-dist": "^3.11.174",
|
||||||
@@ -5511,7 +5512,8 @@
|
|||||||
"node_modules/dayjs": {
|
"node_modules/dayjs": {
|
||||||
"version": "1.11.13",
|
"version": "1.11.13",
|
||||||
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz",
|
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz",
|
||||||
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="
|
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.0",
|
"version": "4.4.0",
|
||||||
@@ -8628,6 +8630,7 @@
|
|||||||
"version": "3.10.1",
|
"version": "3.10.1",
|
||||||
"resolved": "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz",
|
"resolved": "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz",
|
||||||
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
||||||
|
"license": "(MIT OR GPL-3.0-or-later)",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lie": "~3.3.0",
|
"lie": "~3.3.0",
|
||||||
"pako": "~1.0.2",
|
"pako": "~1.0.2",
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"docx-preview": "^0.3.5",
|
"docx-preview": "^0.3.5",
|
||||||
"html-docx-js": "^0.3.1",
|
"html-docx-js": "^0.3.1",
|
||||||
"isbot": "^4.1.0",
|
"isbot": "^4.1.0",
|
||||||
|
"jszip": "^3.10.1",
|
||||||
"mammoth": "^1.9.0",
|
"mammoth": "^1.9.0",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
"pdfjs-dist": "^3.11.174",
|
"pdfjs-dist": "^3.11.174",
|
||||||
|
|||||||
Vendored
+22
File diff suppressed because one or more lines are too long
+2
-2
@@ -23,8 +23,8 @@ export default defineConfig({
|
|||||||
],
|
],
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
port: 5178,
|
port: 5173,
|
||||||
open: true,
|
open: true,
|
||||||
allowedHosts: ['nas.7bm.co', 'localhost', '127.0.0.1'], // 允许的主机名列表
|
// allowedHosts: ['nas.7bm.co', 'localhost', '127.0.0.1'], // 允许的主机名列表
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user