新增提示Toast组件
This commit is contained in:
+7
-7
@@ -219,35 +219,35 @@ export async function apiRequest<T>(
|
||||
if (response.status === 400) {
|
||||
console.error('PostgREST 错误 - 无效请求:', data || responseText);
|
||||
return {
|
||||
error: '无效的请求格式,请检查数据格式是否正确',
|
||||
error: data?.message || data?.msg || '无效的请求格式,请检查数据格式是否正确',
|
||||
status: response.status,
|
||||
headers: responseHeaders
|
||||
};
|
||||
} else if (response.status === 401) {
|
||||
console.error('PostgREST 错误 - 未授权:', data || responseText);
|
||||
return {
|
||||
error: '未授权,请检查认证信息',
|
||||
error: data?.message || data?.msg || '未授权,请检查认证信息',
|
||||
status: response.status,
|
||||
headers: responseHeaders
|
||||
};
|
||||
} else if (response.status === 403) {
|
||||
console.error('PostgREST 错误 - 禁止访问:', data || responseText);
|
||||
return {
|
||||
error: '没有权限执行此操作',
|
||||
error: data?.message || data?.msg || '没有权限执行此操作',
|
||||
status: response.status,
|
||||
headers: responseHeaders
|
||||
};
|
||||
} else if (response.status === 404) {
|
||||
console.error('PostgREST 错误 - 资源不存在:', data || responseText);
|
||||
return {
|
||||
error: '请求的资源不存在',
|
||||
error: data?.message || data?.msg || '请求的资源不存在',
|
||||
status: response.status,
|
||||
headers: responseHeaders
|
||||
};
|
||||
} else {
|
||||
console.error(`HTTP请求失败: ${response.status} - ${url}`, data || responseText);
|
||||
return {
|
||||
error: data?.msg || `请求失败: ${response.status}`,
|
||||
error: data?.message || data?.msg || `请求失败: ${response.status}`,
|
||||
status: response.status,
|
||||
headers: responseHeaders
|
||||
};
|
||||
@@ -256,9 +256,9 @@ export async function apiRequest<T>(
|
||||
|
||||
// 检查API返回的状态码
|
||||
if (data && 'code' in data && data.code !== 0) {
|
||||
console.error(`API请求失败: ${data.msg || '未知错误'} - ${url}`);
|
||||
console.error(`API请求失败: ${data.message || data.msg || '未知错误'} - ${url}`);
|
||||
return {
|
||||
error: data.msg || '请求失败',
|
||||
error: data.message || data.msg || '请求失败',
|
||||
status: response.status,
|
||||
headers: responseHeaders
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { postgrestGet, postgrestDelete, postgrestPost, postgrestPut, type PostgrestParams } from '../postgrest-client';
|
||||
import dayjs from 'dayjs';
|
||||
import { formatDate } from '../../utils';
|
||||
|
||||
// 定义文档类型接口
|
||||
export interface DocumentType {
|
||||
@@ -65,20 +65,6 @@ export interface DocumentTypeSearchParams {
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
* @param dateString 日期字符串
|
||||
* @returns 格式化后的日期字符串
|
||||
*/
|
||||
function formatDate(dateString: string): string {
|
||||
if (!dateString) return '';
|
||||
try {
|
||||
return dayjs(dateString).format('YYYY-MM-DD HH:mm:ss');
|
||||
} catch (error) {
|
||||
console.error('日期格式化失败:', error);
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从不同格式的 API 响应中提取数据
|
||||
|
||||
@@ -90,6 +90,17 @@ interface StatsData {
|
||||
score: number;
|
||||
}
|
||||
|
||||
// 在文件顶部添加的类型定义,在interface区块前添加
|
||||
interface OcrDataResult {
|
||||
pages?: number[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface OcrData {
|
||||
[key: string]: OcrDataResult | unknown;
|
||||
ocr_result?: Record<string, OcrDataResult | unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前评查文件的所有评查点结果
|
||||
* @param fileId 评查文件ID
|
||||
@@ -204,8 +215,9 @@ export async function getReviewPoints(fileId: string) {
|
||||
}
|
||||
|
||||
// 提取页码数组
|
||||
let contentPage: number[] = [];
|
||||
console.log('datacontent-------', data);
|
||||
let contentPage: Record<string, number[]> = {};
|
||||
// console.log('result-------', result.evaluated_results?.result);
|
||||
// console.log('datacontent-------', data);
|
||||
if (data && typeof data === 'object') {
|
||||
try {
|
||||
const dataObj = data as Record<string, string>;
|
||||
@@ -214,19 +226,19 @@ export async function getReviewPoints(fileId: string) {
|
||||
if (Object.prototype.hasOwnProperty.call(dataObj, key)) {
|
||||
// 使用'-'分割获取前缀(如'立案报告表')
|
||||
const prefix = key.split('-')[0];
|
||||
console.log('prefix-------', prefix);
|
||||
// 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>;
|
||||
let ocrData: OcrData = documentData.data.ocrResult as OcrData;
|
||||
|
||||
// 检查是否有嵌套的ocr_result对象
|
||||
if ('ocr_result' in ocrData &&
|
||||
ocrData.ocr_result &&
|
||||
typeof ocrData.ocr_result === 'object') {
|
||||
ocrData = ocrData.ocr_result as Record<string, any>;
|
||||
ocrData = ocrData.ocr_result as OcrData;
|
||||
}
|
||||
|
||||
for (const ocrKey in ocrData) {
|
||||
@@ -239,7 +251,8 @@ export async function getReviewPoints(fileId: string) {
|
||||
// 获取pages数组
|
||||
const pages = ocrData[ocrKey].pages;
|
||||
if (Array.isArray(pages)) {
|
||||
contentPage = pages;
|
||||
// 存储每个key对应的页码数组
|
||||
contentPage[key] = pages;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -249,7 +262,7 @@ export async function getReviewPoints(fileId: string) {
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('解析评查点data失败:', e);
|
||||
contentPage = [];
|
||||
contentPage = {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -420,3 +433,62 @@ export async function updateReviewResult(resultId: string, result: boolean, mess
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认评查结果并更新文档审核状态
|
||||
* @param documentId 文档ID
|
||||
* @returns 更新结果
|
||||
*/
|
||||
export async function confirmReviewResults(documentId: string): Promise<{
|
||||
data?: { auditStatus: number; score: number };
|
||||
error?: string;
|
||||
status?: number;
|
||||
}> {
|
||||
try {
|
||||
if (!documentId) {
|
||||
return { error: '文档ID不能为空', status: 400 };
|
||||
}
|
||||
|
||||
// 获取该文档的所有评查点结果
|
||||
const reviewPointsResponse = await getReviewPoints(documentId);
|
||||
|
||||
if ('error' in reviewPointsResponse && reviewPointsResponse.error) {
|
||||
return { error: reviewPointsResponse.error, status: reviewPointsResponse.status };
|
||||
}
|
||||
|
||||
if (!('data' in reviewPointsResponse) || !reviewPointsResponse.data || !Array.isArray(reviewPointsResponse.data)) {
|
||||
return { error: '获取评查点数据失败', status: 500 };
|
||||
}
|
||||
|
||||
// 计算总分数
|
||||
const totalScore = reviewPointsResponse.stats?.score || 0;
|
||||
|
||||
// 根据总分确定审核状态
|
||||
// <80分:不通过(-1),>=80分:通过(1)
|
||||
const auditStatus = totalScore < 80 ? -1 : 1;
|
||||
|
||||
// 更新文档的审核状态
|
||||
const updateDocumentParams = {
|
||||
audit_status: auditStatus
|
||||
};
|
||||
|
||||
// 调用API更新文档审核状态
|
||||
const response = await postgrestPut<{ id: string }, typeof updateDocumentParams>(
|
||||
'documents',
|
||||
updateDocumentParams,
|
||||
{ id: documentId }
|
||||
);
|
||||
|
||||
if (response.error) {
|
||||
return { error: response.error, status: response.status };
|
||||
}
|
||||
|
||||
return { data: { auditStatus, score: totalScore } };
|
||||
} catch (error) {
|
||||
console.error('确认评查结果失败:', error);
|
||||
return {
|
||||
error: error instanceof Error ? error.message : '确认评查结果失败',
|
||||
status: 500
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { postgrestGet, postgrestPost, postgrestPut, postgrestDelete, type PostgrestParams } from '../postgrest-client';
|
||||
import dayjs from 'dayjs';
|
||||
import { formatDate } from '../../utils';
|
||||
|
||||
/**
|
||||
* 评查点分组接口
|
||||
@@ -44,20 +44,7 @@ interface ApiResponse<T> {
|
||||
data: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
* @param dateString 日期字符串
|
||||
* @returns 格式化后的日期字符串
|
||||
*/
|
||||
function formatDate(dateString: string): string {
|
||||
if (!dateString) return '';
|
||||
try {
|
||||
return dayjs(dateString).format('YYYY-MM-DD HH:mm:ss');
|
||||
} catch (error) {
|
||||
console.error('日期格式化失败:', error);
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从不同格式的 API 响应中提取数据
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getDocumentTypes } from '../document-types/document-types';
|
||||
import type { DocumentTypeUI } from '../document-types/document-types';
|
||||
import weekday from 'dayjs/plugin/weekday';
|
||||
import updateLocale from 'dayjs/plugin/updateLocale';
|
||||
import { formatDate } from '../../utils';
|
||||
|
||||
// 配置 dayjs
|
||||
dayjs.extend(weekday);
|
||||
@@ -110,21 +111,6 @@ interface DocumentReviewResult {
|
||||
issueCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
* @param dateString 日期字符串
|
||||
* @returns 格式化后的日期字符串
|
||||
*/
|
||||
function formatDate(dateString: string): string {
|
||||
if (!dateString) return '';
|
||||
try {
|
||||
return dayjs(dateString).format('YYYY-MM-DD');
|
||||
} catch (error) {
|
||||
console.error('日期格式化失败:', error);
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从不同格式的 API 响应中提取数据
|
||||
* @param responseData API 响应数据
|
||||
@@ -409,6 +395,7 @@ export async function getReviewFiles(searchParams: DocumentSearchParams = {}): P
|
||||
let hasFailResult = false;
|
||||
let totalScore = 0;
|
||||
let totalPoints = 0;
|
||||
let totalPassPoints = 0;
|
||||
|
||||
// 存储该文档的问题消息
|
||||
const issuesList: Array<{severity: 'info' | 'warning' | 'error' | 'critical', message: string}> = [];
|
||||
@@ -425,7 +412,7 @@ export async function getReviewFiles(searchParams: DocumentSearchParams = {}): P
|
||||
}
|
||||
|
||||
// 检查是否有不通过的结果
|
||||
if (resultValue === false) {
|
||||
if (!resultValue) {
|
||||
hasFailResult = true;
|
||||
|
||||
// 收集问题消息
|
||||
@@ -435,6 +422,8 @@ export async function getReviewFiles(searchParams: DocumentSearchParams = {}): P
|
||||
message: evaluatedResults.message as string
|
||||
});
|
||||
}
|
||||
}else{
|
||||
totalPassPoints++;
|
||||
}
|
||||
|
||||
// 计算总分
|
||||
@@ -467,7 +456,9 @@ export async function getReviewFiles(searchParams: DocumentSearchParams = {}): P
|
||||
// 如果没有评查点,默认为通过
|
||||
if (totalPoints > 0) {
|
||||
// 通过分数线为80分
|
||||
status = totalScore >= 80 ? 1 : -1; // 通过或不通过
|
||||
// status = totalScore >= 80 ? 1 : -1; // 通过或不通过
|
||||
// 通过率为80%
|
||||
status = parseFloat((totalPassPoints/totalPoints).toFixed(1)) >= 0.8 ? 1 : -1; // 通过或不通过
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { postgrestGet, postgrestPost, postgrestPut, postgrestDelete, type PostgrestParams } from '../postgrest-client';
|
||||
import dayjs from 'dayjs';
|
||||
import { formatDate } from '../../utils';
|
||||
|
||||
/**
|
||||
* 从不同格式的 API 响应中提取数据
|
||||
@@ -146,11 +146,6 @@ function mapApiRuleToFrontendModel(apiRule: ApiRule): Rule {
|
||||
};
|
||||
}
|
||||
|
||||
// 格式化日期的辅助函数
|
||||
function formatDate(dateString: string): string {
|
||||
return dayjs(dateString).format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取评查点列表
|
||||
* @param params 查询参数
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { postgrestGet, postgrestDelete, postgrestPut, type PostgrestParams } from '../postgrest-client';
|
||||
import dayjs from 'dayjs';
|
||||
import { getDocumentTypes } from '../document-types/document-types';
|
||||
import { formatDate } from '../../utils';
|
||||
|
||||
/**
|
||||
* 从不同格式的 API 响应中提取数据
|
||||
@@ -22,20 +23,6 @@ function extractApiData<T>(responseData: unknown): T | null {
|
||||
return responseData as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
* @param dateString 日期字符串
|
||||
* @returns 格式化后的日期字符串
|
||||
*/
|
||||
function formatDate(dateString: string): string {
|
||||
if (!dateString) return '';
|
||||
try {
|
||||
return dayjs(dateString).format('YYYY-MM-DD HH:mm:ss');
|
||||
} catch (error) {
|
||||
console.error('日期格式化失败:', error);
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询参数
|
||||
@@ -94,7 +81,7 @@ export interface DocumentUI {
|
||||
typeName: string;
|
||||
size: number;
|
||||
auditStatus: number; // -1: 不通过, 0: 待审核, 1: 通过, 2: 警告, 3: 审核中
|
||||
fileStatus: string; // Waiting, Cutting, Extractioning, Evaluationing, Processed
|
||||
fileStatus: string; // Waiting, Cutting, Extractioning, Failed, Evaluationing, Processed
|
||||
issues: number | null;
|
||||
uploadTime: string;
|
||||
fileType: string;
|
||||
@@ -161,6 +148,7 @@ async function convertToUIDocument(doc: Document): Promise<DocumentUI> {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
id: doc.id,
|
||||
name: doc.name,
|
||||
@@ -233,21 +221,19 @@ export async function getDocuments(searchParams: DocumentSearchParams = {}): Pro
|
||||
// 处理日期范围
|
||||
if (searchParams.dateFrom) {
|
||||
// 添加当天开始时间 00:00:00
|
||||
filter['created_at'] = `gte.'${dayjs(`${searchParams.dateFrom} 00:00:00`).format()}'`;
|
||||
filter['created_at'] = `gte.${searchParams.dateFrom + ' 00:00:00'}`;
|
||||
}
|
||||
|
||||
if (searchParams.dateTo) {
|
||||
// 如果有开始日期,使用and条件;否则直接设置结束日期
|
||||
const dateToKey = searchParams.dateFrom ? 'and' : 'created_at';
|
||||
const newDateFrom = dayjs(`${searchParams.dateFrom} 00:00:00`).format();
|
||||
const newDateTo = dayjs(`${searchParams.dateTo} 23:59:59`).format();
|
||||
// 添加当天结束时间 23:59:59
|
||||
if (dateToKey === 'and') {
|
||||
delete filter['created_at'];
|
||||
// 使用OR操作符连接两个条件
|
||||
filter[dateToKey] = `(created_at.gte.'${newDateFrom}',created_at.lte.'${newDateTo}')`;
|
||||
filter[dateToKey] = `(created_at.gte.${searchParams.dateFrom+' 00:00:00'},created_at.lte.${searchParams.dateTo+' 23:59:59'})`;
|
||||
} else {
|
||||
filter['created_at'] = `lte.'${newDateTo}'`;
|
||||
filter['created_at'] = `lte.${searchParams.dateTo+' 23:59:59'}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,6 +253,8 @@ export async function getDocuments(searchParams: DocumentSearchParams = {}): Pro
|
||||
return { error: '获取文档数据失败', status: 500 };
|
||||
}
|
||||
|
||||
// console.log('extractedData---1--',extractedData[0]);
|
||||
|
||||
// 转换为UI格式
|
||||
const documents = await Promise.all(extractedData.map(convertToUIDocument));
|
||||
// console.log('documentsItem',documents)
|
||||
|
||||
@@ -82,6 +82,7 @@ export interface Document {
|
||||
extracted_results?: ExtractedResult;
|
||||
sumary?: Summary;
|
||||
remark?: string;
|
||||
audit_status?: number;
|
||||
}
|
||||
|
||||
// 文件上传响应接口
|
||||
|
||||
+139
-83
@@ -7,29 +7,51 @@ import dayjs from 'dayjs';
|
||||
* @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;
|
||||
if (!responseData) {
|
||||
console.warn('API响应数据为空');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 格式2: 直接是数据对象
|
||||
return responseData as T;
|
||||
}
|
||||
try {
|
||||
// 检查是否有错误信息
|
||||
if (typeof responseData === 'object' && responseData !== null) {
|
||||
// 错误检查: 检查错误码,一般成功的错误码是0或200
|
||||
if ('code' in responseData) {
|
||||
const code = (responseData as { code: number }).code;
|
||||
// 如果有错误码且不是成功状态
|
||||
if (code !== 0 && code !== 200) {
|
||||
const errorMsg = 'msg' in responseData
|
||||
? (responseData as { msg: string }).msg
|
||||
: '未知错误';
|
||||
console.error(`API响应错误: [${code}] ${errorMsg}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 评估结果类型定义
|
||||
*/
|
||||
interface EvaluationResult {
|
||||
result: boolean;
|
||||
rule_id?: string;
|
||||
rule_name?: string;
|
||||
description?: string;
|
||||
[key: string]: unknown;
|
||||
// 错误检查: 检查是否包含错误消息但没有数据
|
||||
if ('error' in responseData && (responseData as { error: unknown }).error) {
|
||||
const error = (responseData as { error: unknown }).error;
|
||||
console.error(`API响应包含错误: ${typeof error === 'string' ? error : JSON.stringify(error)}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 格式1: { code: number, msg: string, data: T }
|
||||
if ('data' in responseData) {
|
||||
const data = (responseData as { data: unknown }).data;
|
||||
if (!data) {
|
||||
console.warn('API响应中的data字段为空');
|
||||
return null;
|
||||
}
|
||||
return data as T;
|
||||
}
|
||||
}
|
||||
|
||||
// 格式2: 直接是数据对象
|
||||
return responseData as T;
|
||||
} catch (error) {
|
||||
console.error('处理API响应数据时出错:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,43 +89,81 @@ export async function getHomeData(): Promise<HomeStatistics> {
|
||||
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');
|
||||
|
||||
// 通用API响应处理函数
|
||||
const handleApiResponse = async <T>(
|
||||
apiCall: Promise<{
|
||||
data?: unknown;
|
||||
headers?: Record<string, string>;
|
||||
error?: string;
|
||||
status?: number
|
||||
}>,
|
||||
errorMessage: string,
|
||||
defaultValue: T
|
||||
): Promise<T> => {
|
||||
try {
|
||||
const response = await apiCall;
|
||||
if (response.error) {
|
||||
console.error(`${errorMessage}: ${response.error}`);
|
||||
return defaultValue;
|
||||
}
|
||||
const data = extractApiData<T>(response.data);
|
||||
if (!data) {
|
||||
console.warn(`${errorMessage}: 无法提取有效数据`);
|
||||
return defaultValue;
|
||||
}
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`${errorMessage}: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
return defaultValue;
|
||||
}
|
||||
};
|
||||
|
||||
// 1. 今日待审核文件 - 获取今天的待审核文件数量 (audit_status = 0 或 2)
|
||||
const todayPendingParams: PostgrestParams = {
|
||||
select: 'count',
|
||||
filter: {
|
||||
or: `(audit_status.eq.0,audit_status.eq.2)`,
|
||||
or: `(audit_status.eq.0,audit_status.eq.2,audit_status.is.null)`,
|
||||
created_at: `gte.${startOfToday}`
|
||||
}
|
||||
};
|
||||
const todayPendingResponse = await postgrestGet('documents', todayPendingParams);
|
||||
const todayPendingCount = extractApiData<{count: number}[]>(todayPendingResponse.data);
|
||||
const todayPendingFiles = todayPendingCount?.[0]?.count || 0;
|
||||
const todayPendingCount = await handleApiResponse<{count: number}[]>(
|
||||
postgrestGet('documents', todayPendingParams),
|
||||
'获取今日待审核文件数量失败',
|
||||
[]
|
||||
);
|
||||
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}`
|
||||
created_at: `gte.${startOfThisMonth}`
|
||||
}
|
||||
};
|
||||
const thisMonthReviewedResponse = await postgrestGet('documents', thisMonthReviewedParams);
|
||||
const thisMonthReviewedCount = extractApiData<{count: number}[]>(thisMonthReviewedResponse.data);
|
||||
const monthlyReviewedFiles = thisMonthReviewedCount?.[0]?.count || 0;
|
||||
const thisMonthReviewedCount = await handleApiResponse<{count: number}[]>(
|
||||
postgrestGet('documents', thisMonthReviewedParams),
|
||||
'获取本月已审核文件数量失败',
|
||||
[]
|
||||
);
|
||||
// 本月已审核文件数量
|
||||
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}`
|
||||
or: `(audit_status.eq.1,audit_status.eq.-1)`,
|
||||
and: `(created_at.gte.${startOfLastMonth},created_at.lte.${endOfLastMonth})`
|
||||
}
|
||||
};
|
||||
const lastMonthReviewedResponse = await postgrestGet('documents', lastMonthReviewedParams);
|
||||
const lastMonthReviewedCount = extractApiData<{count: number}[]>(lastMonthReviewedResponse.data);
|
||||
const lastMonthReviewed = lastMonthReviewedCount?.[0]?.count || 0;
|
||||
const lastMonthReviewedCount = await handleApiResponse<{count: number}[]>(
|
||||
postgrestGet('documents', lastMonthReviewedParams),
|
||||
'获取上月已审核文件数量失败',
|
||||
[]
|
||||
);
|
||||
// 上月已审核文件数量
|
||||
const lastMonthReviewed = lastMonthReviewedCount[0]?.count || 0;
|
||||
|
||||
// 计算同比增长
|
||||
let reviewGrowthValue = 0;
|
||||
@@ -115,37 +175,46 @@ export async function getHomeData(): Promise<HomeStatistics> {
|
||||
reviewGrowthIsUp = growthRate >= 0;
|
||||
}
|
||||
|
||||
// 3. 审核通过率 - 本月审核通过率 (已审核文件 / 总待审核文件 + 已审核文件)
|
||||
// 3. 审核通过率 - 本月审核通过率
|
||||
const thisMonthTotalParams: PostgrestParams = {
|
||||
select: 'count',
|
||||
filter: {
|
||||
created_at: `gte.${startOfThisMonth}`,
|
||||
created_at_lte: `lte.${endOfThisMonth}`
|
||||
audit_status: `eq.1`,
|
||||
created_at: `gte.${startOfThisMonth}`
|
||||
}
|
||||
};
|
||||
const thisMonthTotalResponse = await postgrestGet('documents', thisMonthTotalParams);
|
||||
const thisMonthTotalCount = extractApiData<{count: number}[]>(thisMonthTotalResponse.data);
|
||||
const thisMonthTotal = thisMonthTotalCount?.[0]?.count || 0;
|
||||
const thisMonthTotalCount = await handleApiResponse<{count: number}[]>(
|
||||
postgrestGet('documents', thisMonthTotalParams),
|
||||
'获取本月审核通过数量失败',
|
||||
[]
|
||||
);
|
||||
// 本月审核通过数量
|
||||
const thisMonthPassTotal = thisMonthTotalCount[0]?.count || 0;
|
||||
|
||||
// 本月审核通过率
|
||||
const monthlyPassRate = thisMonthTotal > 0
|
||||
? parseFloat(((monthlyReviewedFiles / thisMonthTotal) * 100).toFixed(1))
|
||||
const monthlyPassRate = (thisMonthPassTotal > 0 && monthlyReviewedFiles > 0)
|
||||
? parseFloat(((thisMonthPassTotal / monthlyReviewedFiles) * 100).toFixed(1))
|
||||
: 0;
|
||||
|
||||
// 上月审核通过率
|
||||
const lastMonthTotalParams: PostgrestParams = {
|
||||
select: 'count',
|
||||
filter: {
|
||||
created_at: `gte.${startOfLastMonth}`,
|
||||
created_at_lte: `lte.${endOfLastMonth}`
|
||||
audit_status: `eq.1`,
|
||||
and: `(created_at.gte.${startOfLastMonth},created_at.lte.${endOfLastMonth})`
|
||||
}
|
||||
};
|
||||
const lastMonthTotalResponse = await postgrestGet('documents', lastMonthTotalParams);
|
||||
const lastMonthTotalCount = extractApiData<{count: number}[]>(lastMonthTotalResponse.data);
|
||||
const lastMonthTotal = lastMonthTotalCount?.[0]?.count || 0;
|
||||
const lastMonthTotalCount = await handleApiResponse<{count: number}[]>(
|
||||
postgrestGet('documents', lastMonthTotalParams),
|
||||
'获取上月审核通过数量失败',
|
||||
[]
|
||||
);
|
||||
// 上月审核通过数量
|
||||
const lastMonthTotal = lastMonthTotalCount[0]?.count || 0;
|
||||
|
||||
// 上月审核通过率
|
||||
const lastMonthPassRate = (lastMonthTotal > 0 && lastMonthReviewed > 0)
|
||||
? (lastMonthReviewed / lastMonthTotal) * 100
|
||||
? parseFloat(((lastMonthTotal / lastMonthReviewed) * 100).toFixed(1))
|
||||
: 0;
|
||||
|
||||
// 计算通过率同比增长
|
||||
@@ -160,48 +229,35 @@ export async function getHomeData(): Promise<HomeStatistics> {
|
||||
|
||||
// 4. 检查出的问题总数(从评估结果表中统计)
|
||||
const thisMonthIssuesParams: PostgrestParams = {
|
||||
select: 'evaluated_results',
|
||||
select: 'count',
|
||||
filter: {
|
||||
created_at: `gte.${startOfThisMonth}`,
|
||||
created_at_lte: `lte.${endOfThisMonth}`
|
||||
and: `(created_at.gte.${startOfThisMonth},created_at.lte.${endOfThisMonth})`,
|
||||
'evaluated_results->result': 'eq.false' // 使用->操作符访问JSONB字段
|
||||
}
|
||||
};
|
||||
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 thisMonthIssuesResponse = await handleApiResponse<{count: number}[]>(
|
||||
postgrestGet('evaluation_results', thisMonthIssuesParams),
|
||||
'获取本月问题数据失败',
|
||||
[]
|
||||
);
|
||||
// 本月问题数量
|
||||
const thisMonthIssuesCount = thisMonthIssuesResponse[0]?.count || 0;
|
||||
|
||||
// 上月问题数量
|
||||
const lastMonthIssuesParams: PostgrestParams = {
|
||||
select: 'evaluated_results',
|
||||
select: 'count',
|
||||
filter: {
|
||||
created_at: `gte.${startOfLastMonth}`,
|
||||
created_at_lte: `lte.${endOfLastMonth}`
|
||||
and: `(created_at.gte.${startOfLastMonth},created_at.lte.${endOfLastMonth})`,
|
||||
'evaluated_results->result': 'eq.false' // 使用->操作符访问JSONB字段
|
||||
}
|
||||
};
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
const lastMonthIssuesResponse = await handleApiResponse<{count: number}[]>(
|
||||
postgrestGet('evaluation_results', lastMonthIssuesParams),
|
||||
'获取上月问题数据失败',
|
||||
[]
|
||||
);
|
||||
// 上月问题数量
|
||||
const lastMonthIssuesCount = lastMonthIssuesResponse[0]?.count || 0;
|
||||
|
||||
// 计算问题数量同比增长
|
||||
let issuesGrowthValue = 0;
|
||||
@@ -233,7 +289,7 @@ export async function getHomeData(): Promise<HomeStatistics> {
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取首页数据失败:', error);
|
||||
console.error('获取首页数据失败:', error instanceof Error ? error.message : String(error));
|
||||
// 返回默认值以防止页面崩溃
|
||||
return {
|
||||
todayPendingFiles: 0,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { postgrestGet, postgrestPut, postgrestPost, postgrestDelete, type PostgrestParams } from '../postgrest-client';
|
||||
import dayjs from 'dayjs';
|
||||
import { formatDate } from '../../utils';
|
||||
|
||||
// 提示词模板接口
|
||||
export interface PromptTemplate {
|
||||
@@ -40,20 +40,6 @@ export interface PromptSearchParams {
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
* @param dateString 日期字符串
|
||||
* @returns 格式化后的日期字符串
|
||||
*/
|
||||
function formatDate(dateString: string): string {
|
||||
if (!dateString) return '';
|
||||
try {
|
||||
return dayjs(dateString).format('YYYY-MM-DD HH:mm:ss');
|
||||
} catch (error) {
|
||||
console.error('日期格式化失败:', error);
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从不同格式的 API 响应中提取数据
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { postgrestGet, postgrestPut, postgrestPost, type PostgrestParams } from '../postgrest-client';
|
||||
import dayjs from 'dayjs';
|
||||
import { formatDate } from '../../utils';
|
||||
// 配置项接口
|
||||
export interface ConfigItem {
|
||||
id: number;
|
||||
@@ -15,20 +15,7 @@ export interface ConfigItem {
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
/**
|
||||
* 格式化日期
|
||||
* @param dateString 日期字符串
|
||||
* @returns 格式化后的日期字符串
|
||||
*/
|
||||
function formatDate(dateString: string): string {
|
||||
if (!dateString) return '';
|
||||
try {
|
||||
return dayjs(dateString).format('YYYY-MM-DD HH:mm:ss');
|
||||
} catch (error) {
|
||||
console.error('日期格式化失败:', error);
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,26 +10,59 @@ interface BreadcrumbProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface PreviousRouteData {
|
||||
title: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
interface Handle {
|
||||
breadcrumb: string | ((data: any) => string);
|
||||
breadcrumb: string | ((data: unknown) => string);
|
||||
previousRoute?: PreviousRouteData | ((data: unknown) => PreviousRouteData | undefined);
|
||||
}
|
||||
|
||||
interface Match {
|
||||
handle?: Handle;
|
||||
pathname: string;
|
||||
data: any;
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
export function Breadcrumb({ items = [], className = '' }: BreadcrumbProps) {
|
||||
const matches = useMatches() as Match[];
|
||||
|
||||
// 构建面包屑数据
|
||||
const breadcrumbs = items.length > 0 ? items : matches
|
||||
.filter(match => match.handle?.breadcrumb)
|
||||
.map(match => ({
|
||||
title: typeof match.handle?.breadcrumb === 'function'
|
||||
? match.handle.breadcrumb(match.data)
|
||||
: match.handle?.breadcrumb,
|
||||
to: match.pathname
|
||||
}));
|
||||
.map((match, index, array) => {
|
||||
// 当前路由的面包屑
|
||||
const current = {
|
||||
title: typeof match.handle?.breadcrumb === 'function'
|
||||
? match.handle.breadcrumb(match.data)
|
||||
: match.handle?.breadcrumb as string,
|
||||
to: match.pathname
|
||||
};
|
||||
|
||||
// 如果当前路由有previousRoute属性且该路由是数组中的最后一个
|
||||
if (match.handle?.previousRoute && index === array.length - 1) {
|
||||
// 获取previousRoute数据,支持函数形式
|
||||
const prevRouteData = typeof match.handle.previousRoute === 'function'
|
||||
? match.handle.previousRoute(match.data)
|
||||
: match.handle.previousRoute;
|
||||
|
||||
// 如果previousRoute存在,添加到面包屑中
|
||||
if (prevRouteData) {
|
||||
return [
|
||||
{
|
||||
title: prevRouteData.title,
|
||||
to: prevRouteData.to
|
||||
},
|
||||
current
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [current];
|
||||
})
|
||||
.flat(); // 扁平化数组
|
||||
|
||||
if (breadcrumbs.length === 0) {
|
||||
return null;
|
||||
|
||||
@@ -9,13 +9,48 @@ interface FileInfoProps {
|
||||
uploadTime?: string;
|
||||
uploadUser?: string;
|
||||
auditStatus?: number;
|
||||
path?: string;
|
||||
};
|
||||
onConfirmResults: () => void;
|
||||
}
|
||||
|
||||
export function FileInfo({ fileInfo, onConfirmResults }: FileInfoProps) {
|
||||
const handleDownloadFile = () => {
|
||||
alert('下载原文件功能');
|
||||
const handleDownloadFile = async () => {
|
||||
try {
|
||||
const urlBefore = 'http://172.18.0.100:9000/docauditai/';
|
||||
const downloadUrl = `${urlBefore}${fileInfo.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 = fileInfo.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 handleExportReport = () => {
|
||||
|
||||
@@ -12,6 +12,27 @@ pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.js';
|
||||
// 导入统一的ReviewPoint类型
|
||||
import { type ReviewPoint } from './';
|
||||
|
||||
/**
|
||||
* 自定义样式
|
||||
* 这些样式解决了PDF页面在放大时互相重叠的问题
|
||||
*/
|
||||
const styles = {
|
||||
pdfContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
position: 'relative' as const,
|
||||
},
|
||||
pageContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
position: 'relative' as const,
|
||||
}
|
||||
};
|
||||
|
||||
// 定义文档内容类型
|
||||
interface FileContent {
|
||||
title: string;
|
||||
@@ -76,24 +97,28 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointId, ta
|
||||
};
|
||||
|
||||
// 当选中的评查点变化时,滚动到对应位置
|
||||
useEffect(() => {
|
||||
if (activeReviewPointId && contentRef.current) {
|
||||
const highlightElement = contentRef.current.querySelector(`[data-review-id="${activeReviewPointId}"]`);
|
||||
if (highlightElement) {
|
||||
highlightElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
// useEffect(() => {
|
||||
// if (activeReviewPointId && contentRef.current) {
|
||||
// const highlightElement = contentRef.current.querySelector(`[data-review-id="${activeReviewPointId}"]`);
|
||||
// if (highlightElement) {
|
||||
// highlightElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
// // highlightElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
|
||||
// 添加临时突出显示效果
|
||||
highlightElement.classList.add('highlight-focus');
|
||||
setTimeout(() => {
|
||||
highlightElement.classList.remove('highlight-focus');
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
}, [activeReviewPointId]);
|
||||
// // 添加临时突出显示效果
|
||||
// highlightElement.classList.add('highlight-focus');
|
||||
// setTimeout(() => {
|
||||
// highlightElement.classList.remove('highlight-focus');
|
||||
// }, 1500);
|
||||
// }
|
||||
// }
|
||||
// }, [activeReviewPointId]);
|
||||
|
||||
// 处理页面跳转
|
||||
const prevTargetPageRef = useRef<number | undefined>(undefined);
|
||||
useEffect(() => {
|
||||
if (targetPage && numPages && targetPage <= numPages) {
|
||||
// 如果有目标页码,并且与上次不同或activeReviewPointId变化了,则执行跳转
|
||||
if (targetPage && numPages && targetPage <= numPages && (targetPage !== prevTargetPageRef.current || activeReviewPointId)) {
|
||||
prevTargetPageRef.current = targetPage;
|
||||
let newTargetPage = targetPage;
|
||||
try {
|
||||
// 安全地访问ocrResult
|
||||
@@ -107,11 +132,11 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointId, ta
|
||||
|
||||
const pageElement = document.getElementById(`page-${newTargetPage}`);
|
||||
if (pageElement) {
|
||||
console.log(`跳转到第${newTargetPage}页`);
|
||||
pageElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
console.log(`跳转到第${newTargetPage}页,对应评查点ID: ${activeReviewPointId}`);
|
||||
pageElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}
|
||||
}, [targetPage, numPages, fileContent]);
|
||||
}, [targetPage, numPages, fileContent, activeReviewPointId]);
|
||||
|
||||
// 获取评查点对应的样式类
|
||||
const getHighlightClass = (status: string) => {
|
||||
@@ -133,6 +158,15 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointId, ta
|
||||
console.log("PDF加载成功,页数:", numPages);
|
||||
}
|
||||
|
||||
// 计算页面在缩放后的实际间距
|
||||
const calculatePageMargin = (zoomFactor: number) => {
|
||||
// 基础间距为30px,随着缩放倍数线性增加
|
||||
const baseMargin = 30;
|
||||
// 页面缩放后,需要额外添加的间距 = (缩放倍数 - 1) * 页面高度
|
||||
const additionalMargin = Math.max(0, (zoomFactor - 1) * 800); // 800是估计的页面高度
|
||||
return baseMargin + additionalMargin;
|
||||
};
|
||||
|
||||
/**
|
||||
* 渲染PDF文档的所有页面
|
||||
*
|
||||
@@ -157,25 +191,35 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointId, ta
|
||||
const pageReviewPoints = reviewPoints.filter(point =>
|
||||
point.position && point.position.section === `page-${i}`
|
||||
);
|
||||
// console.log("pageReviewPoints-------",pageReviewPoints);
|
||||
|
||||
// 计算当前缩放级别下的页面容器样式
|
||||
const zoomFactor = zoomLevel / 100;
|
||||
const pageContainerStyle = {
|
||||
...styles.pageContainer,
|
||||
marginBottom: `${calculatePageMargin(zoomFactor)}px`, // 动态计算页面间距
|
||||
};
|
||||
|
||||
// 为每一页创建组件
|
||||
pages.push(
|
||||
<div key={i} id={`page-${i}`} className="mb-6">
|
||||
<div key={i} id={`page-${i}`} style={pageContainerStyle}>
|
||||
{/* 页码标识,显示在页面上方 */}
|
||||
<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' // 相对定位,作为评查点高亮的定位参考
|
||||
}}>
|
||||
<div
|
||||
className="page-wrapper flex justify-center"
|
||||
style={{
|
||||
transform: `scale(${zoomFactor})`, // 根据zoomLevel应用缩放
|
||||
transformOrigin: 'top center', // 缩放原点设置为顶部中心
|
||||
position: 'relative', // 相对定位,作为评查点高亮的定位参考
|
||||
maxWidth: '100%', // 限制最大宽度
|
||||
}}
|
||||
>
|
||||
{/* 渲染PDF页面组件 */}
|
||||
<Page
|
||||
pageNumber={i} // 当前页码
|
||||
renderTextLayer={true} // 启用文本层,使文本可选择
|
||||
renderAnnotationLayer={true} // 启用注释层,显示PDF内置注释
|
||||
pageNumber={i} // 当前页码
|
||||
renderTextLayer={true} // 启用文本层,使文本可选择
|
||||
renderAnnotationLayer={true} // 启用注释层,显示PDF内置注释
|
||||
className="border border-gray-300 shadow-md" // 添加边框和阴影样式
|
||||
/>
|
||||
|
||||
@@ -218,20 +262,22 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointId, ta
|
||||
// 渲染文档内容
|
||||
const renderDocumentContent = () => {
|
||||
return (
|
||||
<Document
|
||||
file={'http://172.18.0.100:9000/docauditai/'+fileContent.path}
|
||||
onLoadSuccess={onDocumentLoadSuccess}
|
||||
onLoadError={(error) => {
|
||||
console.error("PDF加载错误:", error);
|
||||
setLoadError("PDF文档加载失败:" + (error.message || "未知错误"));
|
||||
}}
|
||||
className="flex flex-col items-center"
|
||||
error={<div className="text-red-500">PDF文档加载失败,请检查链接或网络连接。</div>}
|
||||
noData={<div>无数据</div>}
|
||||
loading={<div className="text-center py-10">PDF加载中...</div>}
|
||||
>
|
||||
{renderAllPages()}
|
||||
</Document>
|
||||
<div style={styles.pdfContainer}>
|
||||
<Document
|
||||
file={'http://172.18.0.100:9000/docauditai/'+fileContent.path}
|
||||
onLoadSuccess={onDocumentLoadSuccess}
|
||||
onLoadError={(error) => {
|
||||
console.error("PDF加载错误:", error);
|
||||
setLoadError("PDF文档加载失败:" + (error.message || "未知错误"));
|
||||
}}
|
||||
className="flex flex-col items-center w-full"
|
||||
error={<div className="text-red-500">PDF文档加载失败,请检查链接或网络连接。</div>}
|
||||
noData={<div>无数据</div>}
|
||||
loading={<div className="text-center py-10">PDF加载中...</div>}
|
||||
>
|
||||
{renderAllPages()}
|
||||
</Document>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -255,15 +301,20 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointId, ta
|
||||
>
|
||||
<i className="ri-zoom-out-line"></i>
|
||||
</button>
|
||||
<button
|
||||
{/* <button
|
||||
className="ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs h-5 leading-5"
|
||||
onClick={toggleHighlights}
|
||||
>
|
||||
<i className="ri-mark-pen-line"></i> {highlightsVisible ? '隐藏问题' : '显示问题'}
|
||||
</button>
|
||||
</button> */}
|
||||
<span className="ml-2 text-xs text-gray-500">{"比例:"+zoomLevel+"%"}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="file-preview-content" ref={contentRef}>
|
||||
<div
|
||||
className="file-preview-content relative overflow-y-auto"
|
||||
ref={contentRef}
|
||||
style={{ maxHeight: 'calc(100vh - 150px)' }}
|
||||
>
|
||||
{loadError ? (
|
||||
<div className="text-red-500 p-4">
|
||||
<p>{loadError}</p>
|
||||
|
||||
@@ -25,6 +25,7 @@ import { useState, useEffect } from 'react';
|
||||
*/
|
||||
export interface ReviewPoint {
|
||||
id: string;
|
||||
pointName: string;
|
||||
title: string;
|
||||
groupName: string;
|
||||
status: string;
|
||||
@@ -34,7 +35,7 @@ export interface ReviewPoint {
|
||||
humanReviewNote?: string;
|
||||
humanReviewBy?: string;
|
||||
humanReviewTime?: string;
|
||||
contentPage?: number[];
|
||||
contentPage?: Record<string, number[]>;
|
||||
position?: {
|
||||
section: string;
|
||||
index: number;
|
||||
@@ -133,7 +134,7 @@ export function ReviewPointsList({
|
||||
// 清除编辑状态
|
||||
setEditingReviewPoint(null);
|
||||
|
||||
alert(`${action === 'approve' ? '通过' : '不通过'}了评查点 ${reviewPointResultId},审核内容: ${message}`);
|
||||
// alert(`${action === 'approve' ? '通过' : '不通过'}了评查点 ${reviewPointResultId},审核内容: ${message}`);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -143,6 +144,7 @@ export function ReviewPointsList({
|
||||
const filteredReviewPoints = reviewPoints.filter(point => {
|
||||
// 匹配搜索文本
|
||||
const matchesSearch = searchText === '' ||
|
||||
point.pointName.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
point.title.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
point.groupName.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
(typeof point.content === 'string' && point.content.toLowerCase().includes(searchText.toLowerCase())) ||
|
||||
@@ -297,7 +299,7 @@ export function ReviewPointsList({
|
||||
<i className="ri-search-line absolute left-2 top-0.5 text-gray-400"></i>
|
||||
{searchText && (
|
||||
<button
|
||||
className="absolute right-2 top-1.5 text-gray-400 hover:text-gray-600"
|
||||
className="absolute right-2 top-0.5 text-gray-400 hover:text-gray-600"
|
||||
onClick={() => setSearchText('')}
|
||||
>
|
||||
<i className="ri-close-line"></i>
|
||||
@@ -430,8 +432,8 @@ export function ReviewPointsList({
|
||||
// 如果当前评查点不处于编辑状态,只显示简单信息
|
||||
if (editingReviewPoint !== reviewPoint.id) {
|
||||
// 根据result和status决定渲染哪种样式
|
||||
if (reviewPoint.result === true || (reviewPoint.result === undefined && reviewPoint.status === 'success')) {
|
||||
// 已通过的评查点只显示基本信息和人工审核注释
|
||||
if (reviewPoint.result === true ){
|
||||
// 已通过的评查点只显示基本信息和人工审核注释 delete
|
||||
if (reviewPoint.needsHumanReview && reviewPoint.humanReviewNote) {
|
||||
return (
|
||||
<div className="mt-2">
|
||||
@@ -449,6 +451,7 @@ export function ReviewPointsList({
|
||||
|
||||
// 处理 result=true 且 postAction=manual 的情况
|
||||
if (reviewPoint.postAction === 'manual') {
|
||||
// 处理重新审核意见的提交
|
||||
const handleReReview = (reviewPointId: string, status: string) => {
|
||||
const note = manualReviewNotes[reviewPointId] || '';
|
||||
if (!note.trim()) {
|
||||
@@ -461,6 +464,7 @@ export function ReviewPointsList({
|
||||
// 可以添加提交成功后的状态更新等操作
|
||||
};
|
||||
|
||||
// 处理重新审核意见的输入
|
||||
const handleNoteChange = (reviewPointId: string, text: string) => {
|
||||
setManualReviewNotes(prev => ({
|
||||
...prev,
|
||||
@@ -503,7 +507,70 @@ export function ReviewPointsList({
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
// 处理 result=true 且 postAction!=manual 的情况
|
||||
return (
|
||||
<>
|
||||
{checkContentPage(reviewPoint).pageIndex === 0 && (
|
||||
<p className="text-xs text-red-500 select-text text-left">该评查点无法找到索引内容,无法自动定位到对应页面。</p>
|
||||
)}
|
||||
<div className="p-2 bg-white rounded border border-gray-200 text-xs mb-3 select-text">
|
||||
{typeof reviewPoint.content === 'object' && reviewPoint.content !== null ? (
|
||||
// 当 content 是对象时的渲染方式
|
||||
<div>
|
||||
{Object.entries(reviewPoint.content).map(([key, value], index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="mb-2 pb-2 border-b border-gray-100 last:border-b-0 last:mb-0 last:pb-0 cursor-pointer hover:bg-gray-100 transition-colors duration-200 rounded p-1"
|
||||
onClick={(e) => {
|
||||
// 阻止事件冒泡,防止触发父元素的点击事件
|
||||
e.stopPropagation();
|
||||
|
||||
console.log(`非通过:单独点击${key}----`,reviewPoint);
|
||||
// 检查评查点是否有 contentPage 以及当前 key 对应的页码数组
|
||||
if (reviewPoint.contentPage && reviewPoint.contentPage[key] && reviewPoint.contentPage[key].length > 0) {
|
||||
// 获取当前 key 对应的第一个页码并跳转
|
||||
console.log(`非通过:单独点击${key}----页码---`,reviewPoint.contentPage[key][0]);
|
||||
|
||||
onReviewPointSelect(reviewPoint.id, reviewPoint.contentPage[key][0]);
|
||||
} else {
|
||||
// 如果没有对应页码,弹出提示
|
||||
// alert(`无法找到"${key}"对应的内容页面`);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
if (reviewPoint.contentPage && reviewPoint.contentPage[key] && reviewPoint.contentPage[key].length > 0) {
|
||||
onReviewPointSelect(reviewPoint.id, reviewPoint.contentPage[key][0]);
|
||||
} else {
|
||||
// alert(`无法找到"${key}"对应的内容页面`);
|
||||
}
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={`查看${key}内容详情`}
|
||||
>
|
||||
{/* 使用flex布局使key和状态标签左右对齐 */}
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-xs">{key}</span>
|
||||
<span className={`text-xs ${value ? 'text-error' : 'text-warning'}`}>
|
||||
{value ? '' : '缺失'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-left select-text">{value || (value === '' ? <span className="invisible">占位符</span> : '')}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
// 当 content 是字符串时的渲染方式
|
||||
<>
|
||||
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// 非通过状态,显示内容和修改建议
|
||||
@@ -512,35 +579,108 @@ export function ReviewPointsList({
|
||||
|
||||
return (
|
||||
<div className="mt-2">
|
||||
|
||||
{/* 没有索引内容提示 */}
|
||||
{reviewPoint.contentPage &&
|
||||
checkContentPage(reviewPoint).pageIndex === 0 && (
|
||||
// <div className="p-2 bg-white rounded border border-gray-200 text-xs mb-3 select-text">
|
||||
<p className="text-xs text-red-500 select-text text-left">该评查点无法找到索引内容,无法自动定位到对应页面。</p>
|
||||
// </div>
|
||||
)}
|
||||
|
||||
{/* 建议内容显示区域 */}
|
||||
{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 text-left">{reviewPoint.suggestion}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* 法律依据内容 */}
|
||||
{reviewPoint.legalBasis && (typeof reviewPoint.legalBasis === 'object') && (
|
||||
(reviewPoint.legalBasis.name || reviewPoint.legalBasis.content ||
|
||||
(reviewPoint.legalBasis.articles && Array.isArray(reviewPoint.legalBasis.articles) && reviewPoint.legalBasis.articles.length > 0)) && (
|
||||
<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">
|
||||
<span className="text-xs font-medium">法律依据</span>
|
||||
</div>
|
||||
{reviewPoint.legalBasis.name && (
|
||||
<p className="text-xs text-left mb-1 select-text">{reviewPoint.legalBasis.name}</p>
|
||||
)}
|
||||
{reviewPoint.legalBasis.content && (
|
||||
<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 && (
|
||||
<div>
|
||||
<p className="text-xs text-left font-medium mb-1">相关条款:</p>
|
||||
<ul className="list-disc pl-4 select-text">
|
||||
{reviewPoint.legalBasis.articles.map((item, index) => (
|
||||
<li key={index} className="text-xs text-left select-text">
|
||||
{typeof item === 'string' ? item :
|
||||
typeof item === 'object' && item !== null ?
|
||||
(item.name ? `${item.name}: ${item.content || ''}` :
|
||||
item.content || JSON.stringify(item)) :
|
||||
String(item)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
|
||||
{reviewPoint.content !== null && (
|
||||
(typeof reviewPoint.content === 'string' && reviewPoint.content !== '') ||
|
||||
(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 select-text">
|
||||
{/* 移除顶部的"当前值"标题,在每个内容项中显示 */}
|
||||
{typeof reviewPoint.content === 'object' && reviewPoint.content !== null ? (
|
||||
// 当 content 是对象时的渲染方式
|
||||
<div>
|
||||
{Object.entries(reviewPoint.content).map(([key, value], index) => (
|
||||
<div key={index} className="mb-2 pb-2 border-b border-gray-100 last:border-b-0 last:mb-0 last:pb-0">
|
||||
<div
|
||||
key={index}
|
||||
className="mb-2 pb-2 border-b border-gray-100 last:border-b-0 last:mb-0 cursor-pointer hover:bg-gray-100 transition-colors duration-200 rounded p-1"
|
||||
onClick={(e) => {
|
||||
// 阻止事件冒泡,防止触发父元素的点击事件
|
||||
e.stopPropagation();
|
||||
|
||||
console.log(`非通过:单独点击${key}----`,reviewPoint);
|
||||
// 检查评查点是否有 contentPage 以及当前 key 对应的页码数组
|
||||
if (reviewPoint.contentPage && reviewPoint.contentPage[key] && reviewPoint.contentPage[key].length > 0) {
|
||||
// 获取当前 key 对应的第一个页码并跳转
|
||||
console.log(`非通过:单独点击${key}----页码---`,reviewPoint.contentPage[key][0]);
|
||||
|
||||
onReviewPointSelect(reviewPoint.id, reviewPoint.contentPage[key][0]);
|
||||
} else {
|
||||
// 如果没有对应页码,弹出提示
|
||||
// alert(`无法找到"${key}"对应的内容页面`);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
if (reviewPoint.contentPage && reviewPoint.contentPage[key] && reviewPoint.contentPage[key].length > 0) {
|
||||
onReviewPointSelect(reviewPoint.id, reviewPoint.contentPage[key][0]);
|
||||
} else {
|
||||
// alert(`无法找到"${key}"对应的内容页面`);
|
||||
}
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={`查看${key}内容详情`}
|
||||
>
|
||||
{/* 使用flex布局使key和状态标签左右对齐 */}
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-xs">{key}</span>
|
||||
{/* <span className={`text-xs ${isErrorStatus ? 'text-error' : 'text-warning'}`}>
|
||||
{isErrorStatus ? '不符合规范' : '需优化'}
|
||||
</span> */}
|
||||
<span className={`text-xs ${isErrorStatus ? 'text-error' : 'text-warning'}`}>
|
||||
{value ? '' : '缺失'}
|
||||
</span>
|
||||
@@ -549,6 +689,8 @@ export function ReviewPointsList({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
) : (
|
||||
// 当 content 是字符串时的渲染方式
|
||||
<>
|
||||
@@ -563,96 +705,65 @@ export function ReviewPointsList({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 法律依据内容 */}
|
||||
{reviewPoint.legalBasis && (typeof reviewPoint.legalBasis === 'object') && (
|
||||
(reviewPoint.legalBasis.name || reviewPoint.legalBasis.content ||
|
||||
(reviewPoint.legalBasis.articles && Array.isArray(reviewPoint.legalBasis.articles) && reviewPoint.legalBasis.articles.length > 0)) && (
|
||||
<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">
|
||||
<span className="text-xs font-medium">法律依据</span>
|
||||
</div>
|
||||
{reviewPoint.legalBasis.name && (
|
||||
<p className="text-xs text-left mb-1 select-text">{reviewPoint.legalBasis.name}</p>
|
||||
)}
|
||||
{reviewPoint.legalBasis.content && (
|
||||
<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 && (
|
||||
<div>
|
||||
<p className="text-xs text-left font-medium mb-1">相关条款:</p>
|
||||
<ul className="list-disc pl-4 select-text">
|
||||
{reviewPoint.legalBasis.articles.map((item, index) => (
|
||||
<li key={index} className="text-xs text-left select-text">
|
||||
{typeof item === 'string' ? item :
|
||||
typeof item === 'object' && item !== null ?
|
||||
(item.name ? `${item.name}: ${item.content || ''}` :
|
||||
item.content || JSON.stringify(item)) :
|
||||
String(item)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* 建议修改区域 */}
|
||||
{/* {(reviewPoint.postAction !== 'none') && ( */}
|
||||
<div className="mb-2">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-gray-700 text-[0.8rem]">{reviewPoint.postAction === 'manual' ? "审核意见:" : "建议修改为:"}</span>
|
||||
{/* <span className="text-green-500">符合规范</span> */}
|
||||
</div>
|
||||
<textarea
|
||||
value={manualReviewNotes[reviewPoint.id] || ''}
|
||||
placeholder={reviewPoint.postAction === 'manual' ? "请输入审核意见(可选)..." : "请输入建议修改内容..."}
|
||||
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 className="flex space-x-2 mt-2">
|
||||
{/* 一键替换按钮 - 只有非人工审核的点或未通过的人工审核点才显示 */}
|
||||
{(reviewPoint.postAction !== 'manual') && (
|
||||
<button
|
||||
className="replace-action flex-1 justify-center"
|
||||
onClick={() => handleReplace(reviewPoint.id)}
|
||||
>
|
||||
<i className="ri-replace-line"></i> 一键替换
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 人工审核按钮 */}
|
||||
{reviewPoint.postAction === 'manual' && (
|
||||
<div className="w-full flex justify-end gap-2">
|
||||
<button
|
||||
className="bg-[#1890ff] hover:bg-blue-600 text-white py-1 px-2 rounded-md text-sm"
|
||||
onClick={() => {
|
||||
const note = manualReviewNotes[reviewPoint.id] || '';
|
||||
handleReviewAction(reviewPoint.id, 'approve', note);
|
||||
}}
|
||||
>
|
||||
<i className="ri-check-line mr-1"></i> 通过
|
||||
</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>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
{/* 建议修改区域 */}
|
||||
{/* {((reviewPoint.postAction === 'manual') || (reviewPoint.content !== null && Object.keys(reviewPoint.content).length > 0)) && ( */}
|
||||
{(reviewPoint.postAction === 'manual') && (
|
||||
<div className="mb-2">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-gray-700 text-[0.8rem]">{reviewPoint.postAction === 'manual' ? "审核意见:" : "建议修改为:"}</span>
|
||||
{/* <span className="text-green-500">符合规范</span> */}
|
||||
</div>
|
||||
<textarea
|
||||
value={manualReviewNotes[reviewPoint.id] || ''}
|
||||
placeholder={reviewPoint.postAction === 'manual' ? "请输入审核意见(可选)..." : "请输入建议修改内容..."}
|
||||
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>
|
||||
)}
|
||||
|
||||
{/* 操作按钮区域 */}
|
||||
{/* {((reviewPoint.postAction === 'manual') || (reviewPoint.content !== null && Object.keys(reviewPoint.content).length > 0)) && ( */}
|
||||
{(reviewPoint.postAction === 'manual') && (
|
||||
<div className="flex space-x-2 mt-2">
|
||||
{/* 一键替换按钮 - 只有非人工审核的点或未通过的人工审核点才显示 */}
|
||||
{(reviewPoint.postAction !== 'manual') && (
|
||||
<button
|
||||
className="replace-action flex-1 justify-center"
|
||||
onClick={() => handleReplace(reviewPoint.id)}
|
||||
>
|
||||
<i className="ri-replace-line"></i> 一键替换
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 人工审核按钮 */}
|
||||
{reviewPoint.postAction === 'manual' && (
|
||||
<div className="w-full flex justify-end gap-2">
|
||||
<button
|
||||
className="bg-[#1890ff] hover:bg-blue-600 text-white py-1 px-2 rounded-md text-sm"
|
||||
onClick={() => {
|
||||
const note = manualReviewNotes[reviewPoint.id] || '';
|
||||
handleReviewAction(reviewPoint.id, 'approve', note);
|
||||
}}
|
||||
>
|
||||
<i className="ri-check-line mr-1"></i> 通过
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -670,12 +781,45 @@ export function ReviewPointsList({
|
||||
// 当 content 是对象时的渲染方式
|
||||
<div>
|
||||
{Object.entries(reviewPoint.content).map(([key, value], index) => (
|
||||
<div key={index} className="mb-2 pb-2 border-b border-gray-100 last:border-b-0 last:mb-0 last:pb-0">
|
||||
<div
|
||||
key={index}
|
||||
className="mb-2 pb-2 border-b border-gray-100 last:border-b-0 last:mb-0 last:pb-0 cursor-pointer hover:bg-gray-50 transition-colors duration-200 rounded p-1"
|
||||
onClick={(e) => {
|
||||
// 阻止事件冒泡,防止触发父元素的点击事件
|
||||
e.stopPropagation();
|
||||
|
||||
console.log(`非通过:单独点击${key}----`,reviewPoint);
|
||||
// 检查评查点是否有 contentPage 以及当前 key 对应的页码数组
|
||||
if (reviewPoint.contentPage && reviewPoint.contentPage[key] && reviewPoint.contentPage[key].length > 0) {
|
||||
// 获取当前 key 对应的第一个页码并跳转
|
||||
console.log(`非通过:单独点击${key}----页码---`,reviewPoint.contentPage[key][0]);
|
||||
|
||||
onReviewPointSelect(reviewPoint.id, reviewPoint.contentPage[key][0]);
|
||||
} else {
|
||||
// 如果没有对应页码,弹出提示
|
||||
alert(`无法找到"${key}"对应的内容页面`);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
if (reviewPoint.contentPage && reviewPoint.contentPage[key] && reviewPoint.contentPage[key].length > 0) {
|
||||
onReviewPointSelect(reviewPoint.id, reviewPoint.contentPage[key][0]);
|
||||
} else {
|
||||
alert(`无法找到"${key}"对应的内容页面`);
|
||||
}
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={`查看${key}内容详情`}
|
||||
>
|
||||
{/* 使用flex布局使key和状态标签左右对齐 */}
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-xs">{key}</span>
|
||||
<span className={`text-xs ${isErrorStatus ? 'text-error' : 'text-warning'}`}>
|
||||
{isErrorStatus ? '不符合规范' : '需优化'}
|
||||
{/* {isErrorStatus ? '不符合规范' : '需优化'} */}
|
||||
{value ? '' : '缺失'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-left select-text">{value || (value === '' ? <span className="invisible">占位符</span> : '')}</p>
|
||||
@@ -770,14 +914,54 @@ export function ReviewPointsList({
|
||||
// 找到被点击的评查点
|
||||
const reviewPoint = reviewPoints.find(point => point.id === id);
|
||||
|
||||
// 如果评查点存在并且有contentPage数组,传递第一个页码
|
||||
if (reviewPoint && reviewPoint.contentPage && reviewPoint.contentPage.length > 0) {
|
||||
onReviewPointSelect(id, reviewPoint.contentPage[0]);
|
||||
// 如果评查点存在
|
||||
if (reviewPoint) {
|
||||
// 使用checkContentPage方法获取页码和key
|
||||
const { pageIndex, key } = checkContentPage(reviewPoint);
|
||||
|
||||
// 如果有有效页码,传递ID和页码
|
||||
if (pageIndex > 0) {
|
||||
console.log(`跳转到页面 ${pageIndex},对应内容 ${key || '未知'}`);
|
||||
onReviewPointSelect(id, pageIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
// 没有有效页码,只传递ID
|
||||
onReviewPointSelect(id);
|
||||
} else {
|
||||
// 没有找到评查点,只传递ID
|
||||
onReviewPointSelect(id);
|
||||
}
|
||||
};
|
||||
|
||||
// 检查评查点的contentPage
|
||||
const checkContentPage = (reviewPoint: ReviewPoint): { pageIndex: number, key?: string, id: string } => {
|
||||
// 返回对象初始化
|
||||
const result = { pageIndex: 0, id: reviewPoint.id };
|
||||
|
||||
// 如果contentPage不存在或是空对象,返回默认值
|
||||
if (!reviewPoint.contentPage || Object.keys(reviewPoint.contentPage).length === 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// 遍历contentPage中的每个key
|
||||
for (const key of Object.keys(reviewPoint.contentPage)) {
|
||||
const pageArr = reviewPoint.contentPage[key];
|
||||
// 如果数组存在且长度大于0
|
||||
if (pageArr && pageArr.length > 0) {
|
||||
// 返回第一个找到的有效页码,以及对应的key
|
||||
return {
|
||||
pageIndex: pageArr[0],
|
||||
key,
|
||||
id: reviewPoint.id
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 如果遍历完所有key都没找到有效页码,返回默认值
|
||||
return result;
|
||||
};
|
||||
|
||||
// 组件主渲染函数
|
||||
return (
|
||||
<div className="review-points-panel select-text">
|
||||
@@ -805,6 +989,8 @@ export function ReviewPointsList({
|
||||
style={{ userSelect: 'text' }}
|
||||
>
|
||||
{/* 评查点标题和状态 */}
|
||||
{/* 评查点名称 pointName*/}
|
||||
<div className="review-point-title flex-1 text-left text-blue-500">{'评查点名称:'+reviewPoint.pointName}</div>
|
||||
<div className="review-point-header flex justify-between items-start">
|
||||
<div className="review-point-title flex-1 text-left">{reviewPoint.title}</div>
|
||||
{/* 评查点所属分组 */}
|
||||
|
||||
@@ -420,15 +420,29 @@ export function ReviewSettings({
|
||||
|
||||
// 处理已删除字段的函数
|
||||
const handleDeletedFields = (deletedFields: string[]) => {
|
||||
console.log("处理已删除字段:", deletedFields);
|
||||
|
||||
// 如果没有删除的字段,则直接返回
|
||||
if (!deletedFields || deletedFields.length === 0) return;
|
||||
|
||||
setRules(prevRules => {
|
||||
return prevRules.map(rule => {
|
||||
const updatedConfig = { ...rule.config };
|
||||
let configModified = false;
|
||||
|
||||
switch (rule.type) {
|
||||
case 'exists':
|
||||
case 'logic':
|
||||
case 'regex':
|
||||
// 从已选字段中移除被删除的字段
|
||||
// 处理存在性判断规则
|
||||
if (Array.isArray(updatedConfig.fields)) {
|
||||
const originalLength = (updatedConfig.fields as string[]).length;
|
||||
// 从fields列表中移除已删除的字段
|
||||
updatedConfig.fields = (updatedConfig.fields as string[]).filter(
|
||||
field => !deletedFields.includes(field)
|
||||
);
|
||||
configModified = originalLength !== (updatedConfig.fields as string[]).length;
|
||||
}
|
||||
|
||||
// 同时处理selectedFields字段(UI显示用)
|
||||
if (Array.isArray(updatedConfig.selectedFields)) {
|
||||
updatedConfig.selectedFields = (updatedConfig.selectedFields as string[]).filter(
|
||||
field => !deletedFields.includes(field)
|
||||
@@ -437,18 +451,52 @@ export function ReviewSettings({
|
||||
break;
|
||||
|
||||
case 'consistency':
|
||||
// 从配对字段中移除被删除的字段
|
||||
// 处理一致性判断规则
|
||||
if (Array.isArray(updatedConfig.pairs)) {
|
||||
const originalLength = (updatedConfig.pairs as ComparisonPair[]).length;
|
||||
// 从配对列表中移除包含已删除字段的配对
|
||||
updatedConfig.pairs = (updatedConfig.pairs as ComparisonPair[]).filter(
|
||||
pair => !deletedFields.includes(pair.sourceField) && !deletedFields.includes(pair.targetField)
|
||||
);
|
||||
configModified = originalLength !== (updatedConfig.pairs as ComparisonPair[]).length;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'format':
|
||||
// 如果判断字段被删除,则清空字段
|
||||
case 'format':
|
||||
// 处理格式判断规则
|
||||
if (updatedConfig.field && deletedFields.includes(updatedConfig.field as string)) {
|
||||
updatedConfig.field = '';
|
||||
configModified = true;
|
||||
}
|
||||
|
||||
if (updatedConfig.checkField && deletedFields.includes(updatedConfig.checkField as string)) {
|
||||
updatedConfig.checkField = '';
|
||||
configModified = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'regex':
|
||||
// 处理正则判断规则
|
||||
if (updatedConfig.field && deletedFields.includes(updatedConfig.field as string)) {
|
||||
updatedConfig.field = '';
|
||||
configModified = true;
|
||||
}
|
||||
|
||||
if (updatedConfig.checkField && deletedFields.includes(updatedConfig.checkField as string)) {
|
||||
updatedConfig.checkField = '';
|
||||
configModified = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'logic':
|
||||
// 处理逻辑判断规则
|
||||
if (Array.isArray(updatedConfig.conditions)) {
|
||||
const originalLength = (updatedConfig.conditions as Condition[]).length;
|
||||
// 从条件列表中移除使用已删除字段的条件
|
||||
updatedConfig.conditions = (updatedConfig.conditions as Condition[]).filter(
|
||||
condition => !deletedFields.includes(condition.field)
|
||||
);
|
||||
configModified = originalLength !== (updatedConfig.conditions as Condition[]).length;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -456,19 +504,30 @@ export function ReviewSettings({
|
||||
break;
|
||||
}
|
||||
|
||||
// 更新可用字段列表,移除被删除的字段
|
||||
// 更新所有规则的可用字段列表
|
||||
if (Array.isArray(updatedConfig.availableFields)) {
|
||||
updatedConfig.availableFields = (updatedConfig.availableFields as string[]).filter(
|
||||
field => !deletedFields.includes(field)
|
||||
);
|
||||
}
|
||||
|
||||
// 如果配置有实质性修改,记录日志
|
||||
if (configModified) {
|
||||
console.log(`规则(ID: ${rule.id}, 类型: ${rule.type})已清除对已删除字段的引用`);
|
||||
}
|
||||
|
||||
return {
|
||||
...rule,
|
||||
config: updatedConfig
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// 在字段删除处理完毕后,触发一次评查配置更新
|
||||
// 使用setTimeout确保状态更新完成后再生成配置
|
||||
setTimeout(() => {
|
||||
generateEvaluationConfig();
|
||||
}, 10);
|
||||
};
|
||||
|
||||
// 更新规则配置中的可用字段但保留已选择的字段和规则配置
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
/**
|
||||
* 全局提示模态框组件
|
||||
*
|
||||
* 用于显示成功、错误、警告和信息提示消息
|
||||
* 支持自动关闭、手动关闭、自定义图标和操作按钮
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import messageModalStyles from '~/styles/components/message-modal.css?url';
|
||||
|
||||
// 消息类型
|
||||
export type MessageType = 'success' | 'error' | 'warning' | 'info';
|
||||
|
||||
// 组件属性
|
||||
interface MessageModalProps {
|
||||
// 是否显示模态框
|
||||
isOpen: boolean;
|
||||
// 关闭模态框的回调
|
||||
onClose: () => void;
|
||||
// 模态框标题
|
||||
title?: string;
|
||||
// 模态框消息内容
|
||||
message: string;
|
||||
// 消息类型
|
||||
type?: MessageType;
|
||||
// 是否自动关闭
|
||||
autoClose?: boolean;
|
||||
// 自动关闭的延迟时间(毫秒)
|
||||
autoCloseDelay?: number;
|
||||
// 确认按钮文本
|
||||
confirmText?: string;
|
||||
// 确认按钮回调
|
||||
onConfirm?: () => void;
|
||||
// 取消按钮文本
|
||||
cancelText?: string;
|
||||
// 是否显示关闭按钮
|
||||
showCloseButton?: boolean;
|
||||
// 自定义图标
|
||||
customIcon?: React.ReactNode;
|
||||
// 自定义内容
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
// 默认自动关闭延迟
|
||||
const DEFAULT_AUTO_CLOSE_DELAY = 3000;
|
||||
|
||||
// 导出样式
|
||||
export function links() {
|
||||
return [{ rel: 'stylesheet', href: messageModalStyles }];
|
||||
}
|
||||
|
||||
export function MessageModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
title,
|
||||
message,
|
||||
type = 'info',
|
||||
autoClose = false,
|
||||
autoCloseDelay = DEFAULT_AUTO_CLOSE_DELAY,
|
||||
confirmText = '确定',
|
||||
onConfirm,
|
||||
cancelText = '取消',
|
||||
showCloseButton = true,
|
||||
customIcon,
|
||||
children
|
||||
}: MessageModalProps) {
|
||||
const [isClosing, setIsClosing] = useState(false);
|
||||
const [portalElement, setPortalElement] = useState<HTMLElement | null>(null);
|
||||
|
||||
// 在客户端渲染时获取 portal 容器
|
||||
useEffect(() => {
|
||||
if (typeof document !== 'undefined') {
|
||||
let element = document.getElementById('message-modal-portal');
|
||||
if (!element) {
|
||||
element = document.createElement('div');
|
||||
element.id = 'message-modal-portal';
|
||||
document.body.appendChild(element);
|
||||
}
|
||||
setPortalElement(element);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 处理关闭动画
|
||||
const handleClose = useCallback(() => {
|
||||
setIsClosing(true);
|
||||
setTimeout(() => {
|
||||
setIsClosing(false);
|
||||
onClose();
|
||||
}, 300); // 动画持续时间
|
||||
}, [onClose]);
|
||||
|
||||
// 处理确认
|
||||
const handleConfirm = useCallback(() => {
|
||||
if (onConfirm) {
|
||||
onConfirm();
|
||||
}
|
||||
handleClose();
|
||||
}, [onConfirm, handleClose]);
|
||||
|
||||
// 自动关闭
|
||||
useEffect(() => {
|
||||
if (isOpen && autoClose) {
|
||||
const timer = setTimeout(() => {
|
||||
handleClose();
|
||||
}, autoCloseDelay);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isOpen, autoClose, autoCloseDelay, handleClose]);
|
||||
|
||||
// 关闭按钮键盘交互
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
handleClose();
|
||||
}
|
||||
}, [handleClose]);
|
||||
|
||||
// 渲染图标
|
||||
const renderIcon = () => {
|
||||
if (customIcon) {
|
||||
return customIcon;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'success':
|
||||
return <i className="ri-check-line message-modal-icon success"></i>;
|
||||
case 'error':
|
||||
return <i className="ri-close-circle-line message-modal-icon error"></i>;
|
||||
case 'warning':
|
||||
return <i className="ri-alert-line message-modal-icon warning"></i>;
|
||||
case 'info':
|
||||
default:
|
||||
return <i className="ri-information-line message-modal-icon info"></i>;
|
||||
}
|
||||
};
|
||||
|
||||
// 如果模态框未打开,不渲染
|
||||
if (!isOpen || !portalElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 使用 Portal 渲染模态框
|
||||
return createPortal(
|
||||
<div
|
||||
className={`message-modal-overlay ${isClosing ? 'closing' : ''}`}
|
||||
onClick={handleClose}
|
||||
onKeyDown={handleKeyDown}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
aria-label="关闭对话框"
|
||||
>
|
||||
<div
|
||||
className={`message-modal message-modal-${type} ${isClosing ? 'closing' : ''}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="message-modal-title"
|
||||
aria-describedby="message-modal-content"
|
||||
>
|
||||
{showCloseButton && (
|
||||
<button
|
||||
className="message-modal-close"
|
||||
onClick={handleClose}
|
||||
aria-label="关闭"
|
||||
>
|
||||
<i className="ri-close-line"></i>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="message-modal-icon-wrapper">
|
||||
{renderIcon()}
|
||||
</div>
|
||||
|
||||
<div className="message-modal-content">
|
||||
{title && (
|
||||
<h3 id="message-modal-title" className="message-modal-title">
|
||||
{title}
|
||||
</h3>
|
||||
)}
|
||||
|
||||
<div id="message-modal-content" className="message-modal-message">
|
||||
{message}
|
||||
</div>
|
||||
|
||||
{children && (
|
||||
<div className="message-modal-custom-content">
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="message-modal-actions">
|
||||
{onConfirm && (
|
||||
<>
|
||||
<button
|
||||
className="message-modal-button primary"
|
||||
onClick={handleConfirm}
|
||||
>
|
||||
{confirmText}
|
||||
</button>
|
||||
<button
|
||||
className="message-modal-button"
|
||||
onClick={handleClose}
|
||||
>
|
||||
{cancelText}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!onConfirm && (
|
||||
<button
|
||||
className="message-modal-button primary"
|
||||
onClick={handleClose}
|
||||
>
|
||||
{confirmText}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
portalElement
|
||||
);
|
||||
}
|
||||
|
||||
// 创建全局消息服务
|
||||
type ShowMessageOptions = Omit<MessageModalProps, 'isOpen' | 'onClose'>;
|
||||
|
||||
class MessageService {
|
||||
private static instance: MessageService;
|
||||
private showModal: ((options: ShowMessageOptions) => void) | null = null;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static getInstance(): MessageService {
|
||||
if (!MessageService.instance) {
|
||||
MessageService.instance = new MessageService();
|
||||
}
|
||||
return MessageService.instance;
|
||||
}
|
||||
|
||||
registerShowModal(showFn: (options: ShowMessageOptions) => void): void {
|
||||
this.showModal = showFn;
|
||||
}
|
||||
|
||||
success(message: string, options?: Partial<ShowMessageOptions>): void {
|
||||
this.show({ ...options, message, type: 'success' });
|
||||
}
|
||||
|
||||
error(message: string, options?: Partial<ShowMessageOptions>): void {
|
||||
this.show({ ...options, message, type: 'error' });
|
||||
}
|
||||
|
||||
warning(message: string, options?: Partial<ShowMessageOptions>): void {
|
||||
this.show({ ...options, message, type: 'warning' });
|
||||
}
|
||||
|
||||
info(message: string, options?: Partial<ShowMessageOptions>): void {
|
||||
this.show({ ...options, message, type: 'info' });
|
||||
}
|
||||
|
||||
show(options: ShowMessageOptions): void {
|
||||
if (this.showModal) {
|
||||
this.showModal(options);
|
||||
} else {
|
||||
console.error('MessageService: showModal is not registered');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const messageService = MessageService.getInstance();
|
||||
|
||||
/**
|
||||
* 全局消息模态框容器
|
||||
* 用于在应用根组件中挂载消息模态框服务
|
||||
*/
|
||||
export function MessageModalProvider({ children }: { children: React.ReactNode }) {
|
||||
const [messageOptions, setMessageOptions] = useState<ShowMessageOptions | null>(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
messageService.registerShowModal((options) => {
|
||||
setMessageOptions(options);
|
||||
setIsOpen(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleClose = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
{messageOptions && (
|
||||
<MessageModal
|
||||
{...messageOptions}
|
||||
isOpen={isOpen}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
/**
|
||||
* 轻量级顶部通知组件
|
||||
*
|
||||
* 在页面顶部显示简单的提示信息,带有图标,自动换行,有最大宽度和高度限制
|
||||
* 支持自动关闭、点击关闭
|
||||
* 能根据文字长度自动调整宽度,超出最大宽度则自动换行
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import toastStyles from '~/styles/components/toast.css?url';
|
||||
|
||||
// 通知类型
|
||||
export type ToastType = 'success' | 'error' | 'warning' | 'info';
|
||||
|
||||
// 组件属性
|
||||
interface ToastProps {
|
||||
// 是否显示通知
|
||||
isOpen: boolean;
|
||||
// 关闭通知的回调
|
||||
onClose: () => void;
|
||||
// 通知内容
|
||||
message: string;
|
||||
// 通知类型
|
||||
type?: ToastType;
|
||||
// 是否自动关闭
|
||||
autoClose?: boolean;
|
||||
// 自动关闭的延迟时间(毫秒)
|
||||
autoCloseDelay?: number;
|
||||
// 自定义图标
|
||||
customIcon?: React.ReactNode;
|
||||
// 自定义CSS类名
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// 默认自动关闭延迟
|
||||
const DEFAULT_AUTO_CLOSE_DELAY = 3000;
|
||||
|
||||
// 导出样式
|
||||
export function links() {
|
||||
return [{ rel: 'stylesheet', href: toastStyles }];
|
||||
}
|
||||
|
||||
export function Toast({
|
||||
isOpen,
|
||||
onClose,
|
||||
message,
|
||||
type = 'info',
|
||||
autoClose = true,
|
||||
autoCloseDelay = DEFAULT_AUTO_CLOSE_DELAY,
|
||||
customIcon,
|
||||
className = ''
|
||||
}: ToastProps) {
|
||||
const [isClosing, setIsClosing] = useState(false);
|
||||
const [portalElement, setPortalElement] = useState<HTMLElement | null>(null);
|
||||
const [messageLines, setMessageLines] = useState<number>(1);
|
||||
|
||||
// 在客户端渲染时获取 portal 容器
|
||||
useEffect(() => {
|
||||
if (typeof document !== 'undefined') {
|
||||
let element = document.getElementById('toast-portal');
|
||||
if (!element) {
|
||||
element = document.createElement('div');
|
||||
element.id = 'toast-portal';
|
||||
element.className = 'toast-container';
|
||||
document.body.appendChild(element);
|
||||
}
|
||||
setPortalElement(element);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 计算消息行数(用于可能的额外样式调整)
|
||||
useEffect(() => {
|
||||
if (message) {
|
||||
// 简单估算行数: 假设每行平均40个字符,+1确保有足够空间
|
||||
const estimatedLines = Math.ceil(message.length / 40) + 1;
|
||||
setMessageLines(Math.max(1, Math.min(estimatedLines, 10))); // 最小1行,最大10行
|
||||
}
|
||||
}, [message]);
|
||||
|
||||
// 处理关闭动画
|
||||
const handleClose = useCallback(() => {
|
||||
setIsClosing(true);
|
||||
setTimeout(() => {
|
||||
setIsClosing(false);
|
||||
onClose();
|
||||
}, 300); // 动画持续时间
|
||||
}, [onClose]);
|
||||
|
||||
// 自动关闭
|
||||
useEffect(() => {
|
||||
if (isOpen && autoClose) {
|
||||
// 根据消息长度调整显示时间,长消息显示更长时间
|
||||
const adjustedDelay = Math.min(
|
||||
autoCloseDelay + (message.length > 100 ? 2000 : 0),
|
||||
10000 // 最长不超过10秒
|
||||
);
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
handleClose();
|
||||
}, adjustedDelay);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isOpen, autoClose, autoCloseDelay, handleClose, message]);
|
||||
|
||||
// 渲染图标
|
||||
const renderIcon = () => {
|
||||
if (customIcon) {
|
||||
return customIcon;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'success':
|
||||
return <i className="ri-check-line toast-icon success"></i>;
|
||||
case 'error':
|
||||
return <i className="ri-close-circle-line toast-icon error"></i>;
|
||||
case 'warning':
|
||||
return <i className="ri-alert-line toast-icon warning"></i>;
|
||||
case 'info':
|
||||
default:
|
||||
return <i className="ri-information-line toast-icon info"></i>;
|
||||
}
|
||||
};
|
||||
|
||||
// 键盘事件处理
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
handleClose();
|
||||
}
|
||||
}, [handleClose]);
|
||||
|
||||
// 如果通知未打开,不渲染
|
||||
if (!isOpen || !portalElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 使用 Portal 渲染通知
|
||||
return createPortal(
|
||||
<div
|
||||
className={`toast toast-${type} ${isClosing ? 'closing' : ''} ${className} ${messageLines > 3 ? 'toast-multiline' : ''}`}
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
onKeyDown={handleKeyDown}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="toast-content">
|
||||
<div className="toast-icon-wrapper">
|
||||
{renderIcon()}
|
||||
</div>
|
||||
<div className="toast-message">
|
||||
{message}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="toast-close"
|
||||
onClick={handleClose}
|
||||
aria-label="关闭"
|
||||
>
|
||||
<i className="ri-close-line"></i>
|
||||
</button>
|
||||
</div>,
|
||||
portalElement
|
||||
);
|
||||
}
|
||||
|
||||
// 创建全局通知服务
|
||||
type ShowToastOptions = Omit<ToastProps, 'isOpen' | 'onClose'>;
|
||||
|
||||
class ToastService {
|
||||
private static instance: ToastService;
|
||||
private showToast: ((options: ShowToastOptions) => void) | null = null;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static getInstance(): ToastService {
|
||||
if (!ToastService.instance) {
|
||||
ToastService.instance = new ToastService();
|
||||
}
|
||||
return ToastService.instance;
|
||||
}
|
||||
|
||||
registerShowToast(showFn: (options: ShowToastOptions) => void): void {
|
||||
this.showToast = showFn;
|
||||
}
|
||||
|
||||
success(message: string, options?: Partial<ShowToastOptions>): void {
|
||||
this.show({ ...options, message, type: 'success' });
|
||||
}
|
||||
|
||||
error(message: string, options?: Partial<ShowToastOptions>): void {
|
||||
this.show({ ...options, message, type: 'error' });
|
||||
}
|
||||
|
||||
warning(message: string, options?: Partial<ShowToastOptions>): void {
|
||||
this.show({ ...options, message, type: 'warning' });
|
||||
}
|
||||
|
||||
info(message: string, options?: Partial<ShowToastOptions>): void {
|
||||
this.show({ ...options, message, type: 'info' });
|
||||
}
|
||||
|
||||
show(options: ShowToastOptions): void {
|
||||
if (this.showToast) {
|
||||
this.showToast(options);
|
||||
} else {
|
||||
console.error('ToastService: showToast is not registered');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const toastService = ToastService.getInstance();
|
||||
|
||||
/**
|
||||
* 全局通知容器
|
||||
* 用于在应用根组件中挂载通知服务
|
||||
*/
|
||||
export function ToastProvider({ children }: { children: React.ReactNode }) {
|
||||
const [toastQueue, setToastQueue] = useState<(ShowToastOptions & { id: string })[]>([]);
|
||||
const maxToasts = 5; // 增加最大显示的通知数量
|
||||
|
||||
useEffect(() => {
|
||||
toastService.registerShowToast((options) => {
|
||||
const id = Math.random().toString(36).substring(2, 9);
|
||||
|
||||
setToastQueue(prev => {
|
||||
// 如果已经有太多通知,移除最早的
|
||||
if (prev.length >= maxToasts) {
|
||||
return [...prev.slice(1), { ...options, id }];
|
||||
}
|
||||
return [...prev, { ...options, id }];
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleCloseToast = (id: string) => {
|
||||
setToastQueue(prev => prev.filter(toast => toast.id !== id));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
{toastQueue.map((toast) => (
|
||||
<Toast
|
||||
key={toast.id}
|
||||
isOpen={true}
|
||||
onClose={() => handleCloseToast(toast.id)}
|
||||
message={toast.message}
|
||||
type={toast.type}
|
||||
autoClose={toast.autoClose !== undefined ? toast.autoClose : true}
|
||||
autoCloseDelay={toast.autoCloseDelay}
|
||||
customIcon={toast.customIcon}
|
||||
className={toast.className}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
+13
-3
@@ -12,9 +12,13 @@ import {
|
||||
} from "@remix-run/react";
|
||||
import { Layout } from "~/components/layout/Layout";
|
||||
import { ErrorBoundary as AppErrorBoundary } from "~/components/error/ErrorBoundary";
|
||||
import { MessageModalProvider } from "~/components/ui/MessageModal";
|
||||
import { ToastProvider } from "~/components/ui/Toast";
|
||||
import "remixicon/fonts/remixicon.css";
|
||||
// 导入样式
|
||||
import styles from "~/styles/main.css?url";
|
||||
import messageModalStyles from "~/styles/components/message-modal.css?url";
|
||||
import toastStyles from "~/styles/components/toast.css?url";
|
||||
|
||||
// 添加客户端hydration错误处理
|
||||
// if (typeof window !== "undefined") {
|
||||
@@ -43,6 +47,8 @@ export const meta: MetaFunction = () => {
|
||||
export function links() {
|
||||
return [
|
||||
{ rel: "stylesheet", href: styles },
|
||||
{ rel: "stylesheet", href: messageModalStyles },
|
||||
{ rel: "stylesheet", href: toastStyles },
|
||||
// { rel: "preconnect", href: "https://fonts.googleapis.com" },
|
||||
// { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" },
|
||||
// { rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap" }
|
||||
@@ -72,9 +78,13 @@ export default function App() {
|
||||
<Links />
|
||||
</head>
|
||||
<body className="font-sans">
|
||||
<Layout>
|
||||
<Outlet />
|
||||
</Layout>
|
||||
<MessageModalProvider>
|
||||
<ToastProvider>
|
||||
<Layout>
|
||||
<Outlet />
|
||||
</Layout>
|
||||
</ToastProvider>
|
||||
</MessageModalProvider>
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
</body>
|
||||
|
||||
+19
-31
@@ -9,6 +9,7 @@ import { Tag } from "~/components/ui/Tag";
|
||||
import homeStyles from "~/styles/pages/home.css?url";
|
||||
import { getDocuments, type DocumentUI } from "~/api/files/documents";
|
||||
import { useState, useEffect } from "react";
|
||||
import { getHomeData } from "~/api/home/home";
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
// 文件处理状态选项
|
||||
@@ -40,13 +41,6 @@ interface StatsData {
|
||||
passRate: number;
|
||||
}
|
||||
|
||||
|
||||
// interface LoaderData {
|
||||
// stats: StatsData;
|
||||
// recentFiles: RecentFile[];
|
||||
// }
|
||||
|
||||
|
||||
// 模拟数据,实际项目中应该从API获取
|
||||
export async function loader() {
|
||||
try {
|
||||
@@ -66,17 +60,11 @@ export async function loader() {
|
||||
console.log("recentFiles-------",recentFiles);
|
||||
|
||||
|
||||
// 模拟数据
|
||||
const stats = {
|
||||
totalFiles: 156,
|
||||
reviewedFiles: 124,
|
||||
pendingFiles: 32,
|
||||
passRate: 92.5
|
||||
} as StatsData;
|
||||
|
||||
const homeData = await getHomeData();
|
||||
console.log("homeData-------",homeData);
|
||||
|
||||
|
||||
return Response.json({ stats, recentFiles });
|
||||
return Response.json({ homeData, recentFiles });
|
||||
} catch (error) {
|
||||
// 错误处理
|
||||
console.error('Failed to fetch dashboard data:', error);
|
||||
@@ -88,7 +76,7 @@ export async function loader() {
|
||||
}
|
||||
|
||||
export default function Index() {
|
||||
const { stats, recentFiles } = useLoaderData<typeof loader>();
|
||||
const { homeData, recentFiles } = useLoaderData<typeof loader>();
|
||||
const [currentDateTime, setCurrentDateTime] = useState<{
|
||||
date: string;
|
||||
time: string;
|
||||
@@ -135,26 +123,26 @@ export default function Index() {
|
||||
<div className="stat-grid ">
|
||||
<StatCard
|
||||
title="今日待审文件"
|
||||
value={stats.totalFiles}
|
||||
icon="ri-file-list-3-line"
|
||||
/>
|
||||
value={homeData.todayPendingFiles}
|
||||
icon="ri-inbox-line"
|
||||
/>
|
||||
<StatCard
|
||||
title="本月已审核文件"
|
||||
value={stats.reviewedFiles}
|
||||
icon="ri-check-double-line"
|
||||
trend={{ value: 5.2, isUp: true }}
|
||||
value={homeData.monthlyReviewedFiles}
|
||||
icon="ri-file-search-line"
|
||||
trend={{ value: homeData.monthlyReviewGrowth.value, isUp: homeData.monthlyReviewGrowth.isUp }}
|
||||
/>
|
||||
<StatCard
|
||||
title="审核通过率"
|
||||
value={stats.pendingFiles}
|
||||
icon="ri-time-line"
|
||||
trend={{ value: 2.1, isUp: false }}
|
||||
title="本月审核通过率"
|
||||
value={homeData.monthlyPassRate}
|
||||
icon="ri-percent-line"
|
||||
trend={{ value: homeData.passRateGrowth.value, isUp: homeData.passRateGrowth.isUp }}
|
||||
/>
|
||||
<StatCard
|
||||
title="问题检出数"
|
||||
value={`${stats.passRate}%`}
|
||||
icon="ri-pie-chart-line"
|
||||
trend={{ value: 1.5, isUp: true }}
|
||||
title="本月问题检出数"
|
||||
value={homeData.issuesDetected}
|
||||
icon="ri-error-warning-line"
|
||||
trend={{ value: homeData.issuesGrowth.value, isUp: homeData.issuesGrowth.isUp }}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -373,6 +373,36 @@ export default function DocumentTypesList() {
|
||||
onChange={handleGroupChange}
|
||||
className="flex-1 min-w-[200px]"
|
||||
/>
|
||||
|
||||
{/* <FilterSelect
|
||||
label="评查点类型"
|
||||
name="ruleType"
|
||||
value={searchParams.get('ruleType') || ''}
|
||||
options={[
|
||||
...ruleTypes.map((type: ApiRuleType) => ({
|
||||
value: type.id,
|
||||
label: type.name
|
||||
}))
|
||||
]}
|
||||
onChange={handleFilterChange}
|
||||
className="mr-3 w-[20%]"
|
||||
/>
|
||||
|
||||
<FilterSelect
|
||||
label="所属规则组"
|
||||
name="groupId"
|
||||
value={searchParams.get('groupId') || ''}
|
||||
options={[
|
||||
...(isRuleGroupSelectDisabled ? [{ value: "", label: "请先选择评查点类型" }] : []),
|
||||
...ruleGroups.map(group => ({
|
||||
value: group.id,
|
||||
label: group.name
|
||||
}))
|
||||
]}
|
||||
onChange={handleFilterChange}
|
||||
className={`mr-3 w-[20%] ${isRuleGroupSelectDisabled ? 'opacity-50' : ''}`}
|
||||
/> */}
|
||||
|
||||
</FilterPanel>
|
||||
|
||||
{/* 数据表格 */}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useSearchParams, useLoaderData, useFetcher, useNavigate,Link } from "@remix-run/react";
|
||||
import { type MetaFunction, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { Card } from "~/components/ui/Card";
|
||||
@@ -12,6 +12,7 @@ import documentsIndexStyles from "~/styles/pages/documents_index.css?url";
|
||||
import { getDocuments, deleteDocument, type DocumentUI } from "~/api/files/documents";
|
||||
import { getDocumentTypes } from "~/api/document-types/document-types";
|
||||
import { updateDocumentAuditStatus } from "~/api/evaluation_points/rules-files";
|
||||
import { toastService } from "~/components/ui/Toast";
|
||||
|
||||
// 导入样式
|
||||
export function links() {
|
||||
@@ -80,6 +81,12 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
});
|
||||
};
|
||||
|
||||
// 定义action返回的数据类型
|
||||
interface ActionResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// 处理表单提交和删除等操作
|
||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
const formData = await request.formData();
|
||||
@@ -92,7 +99,6 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
if (response.error) {
|
||||
return Response.json({ success: false, message: response.error }, { status: response.status || 500 });
|
||||
}
|
||||
|
||||
return Response.json({ success: true, message: "文档已成功删除" });
|
||||
}
|
||||
|
||||
@@ -109,7 +115,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
message: `删除失败: ${failures.map(f => f.error).join(', ')}`
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
|
||||
return Response.json({ success: true, message: `已成功删除${ids.length}个文档` });
|
||||
}
|
||||
|
||||
@@ -133,6 +139,7 @@ const fileProcessingStatusOptions = [
|
||||
{ value: "Cutting", label: "切分中", icon: "ri-loader-line", color: "purple" },
|
||||
{ value: "Extractioning", label: "抽取中", icon: "ri-loader-line", color: "cyan" },
|
||||
{ value: "Evaluationing", label: "评查中", icon: "ri-loader-line", color: "teal" },
|
||||
{ value: "Failed", label: "抽取异常", icon: "ri-close-circle-line", color: "red" },
|
||||
{ value: "Processed", label: "已完成", icon: "ri-check-line", color: "green" },
|
||||
];
|
||||
|
||||
@@ -143,6 +150,7 @@ const fileStatusOptions = [
|
||||
{ value: "Cutting", label: "切分中" },
|
||||
{ value: "Extractioning", label: "抽取中" },
|
||||
{ value: "Evaluationing", label: "评查中" },
|
||||
{ value: "Failed", label: "抽取异常" },
|
||||
{ value: "Processed", label: "已完成" },
|
||||
];
|
||||
|
||||
@@ -182,7 +190,7 @@ export default function DocumentsIndex() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
|
||||
const loaderData = useLoaderData<typeof loader>();
|
||||
const fetcher = useFetcher();
|
||||
const fetcher = useFetcher<ActionResponse>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 从URL获取当前筛选条件
|
||||
@@ -363,27 +371,26 @@ export default function DocumentsIndex() {
|
||||
|
||||
// 删除文档
|
||||
const handleDelete = (id: string, name: string, fileStatus: string) => {
|
||||
if (fileStatus !== 'Processed') {
|
||||
alert('文档正在处理中,不能删除');
|
||||
// 禁止删除处理中的文件
|
||||
if (fileStatus !== "Processed" && fileStatus !== "Failed") {
|
||||
toastService.warning("文件正在处理中,无法删除");
|
||||
return;
|
||||
}
|
||||
if (window.confirm(`确认删除文档 "${name}"?`)) {
|
||||
// 使用fetcher提交表单
|
||||
const formData = new FormData();
|
||||
formData.append('_action', 'delete');
|
||||
formData.append('id', id);
|
||||
|
||||
if (confirm(`确定要删除文档"${name}"吗?`)) {
|
||||
const form = new FormData();
|
||||
form.append("_action", "delete");
|
||||
form.append("id", id);
|
||||
|
||||
fetcher.submit(formData, { method: 'post' });
|
||||
|
||||
// 更新选中行
|
||||
setSelectedRowKeys(selectedRowKeys.filter(key => key !== id));
|
||||
fetcher.submit(form, { method: "post" });
|
||||
}
|
||||
};
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedRowKeys.length === 0) {
|
||||
alert('请至少选择一个文档');
|
||||
// alert('请至少选择一个文档');
|
||||
toastService.error('请至少选择一个文档');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -393,7 +400,8 @@ export default function DocumentsIndex() {
|
||||
);
|
||||
|
||||
if (hasProcessingFiles) {
|
||||
alert('存在服务器未处理完成的文件,请重新选择需要删除的文件');
|
||||
// alert('存在服务器未处理完成的文件,请重新选择需要删除的文件');
|
||||
toastService.error('存在服务器未处理完成的文件,请重新选择需要删除的文件');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -430,7 +438,8 @@ export default function DocumentsIndex() {
|
||||
const handleExport = async () => {
|
||||
// 如果没有文档,显示提示信息
|
||||
if (documents.length === 0) {
|
||||
alert('当前页面没有文档可供导出');
|
||||
// alert('当前页面没有文档可供导出');
|
||||
toastService.error('当前页面没有文档可供导出');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -480,7 +489,8 @@ export default function DocumentsIndex() {
|
||||
const failed = results.filter(r => r && !r.success).length;
|
||||
|
||||
if (succeeded === 0) {
|
||||
alert('所有文件下载失败');
|
||||
// alert('所有文件下载失败');
|
||||
toastService.error('所有文件下载失败');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -503,36 +513,51 @@ export default function DocumentsIndex() {
|
||||
|
||||
// 显示结果消息
|
||||
if (failed > 0) {
|
||||
alert(`成功导出 ${succeeded} 个文件,${failed} 个文件失败`);
|
||||
// alert(`成功导出 ${succeeded} 个文件,${failed} 个文件失败`);
|
||||
toastService.warning(`成功导出 ${succeeded} 个文件,${failed} 个文件失败`);
|
||||
} else {
|
||||
alert(`成功导出 ${succeeded} 个文件`);
|
||||
// alert(`成功导出 ${succeeded} 个文件`);
|
||||
toastService.success(`成功导出 ${succeeded} 个文件`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('导出文件失败:', error);
|
||||
alert(`导出文件失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
// alert(`导出文件失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
toastService.error(`导出文件失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 开始审核
|
||||
const handleReviewFileClick = async (fileId: number, auditStatus: number | null) => {
|
||||
// 检查audit_status是否为0,如果是则更新为2
|
||||
if (auditStatus === 0) {
|
||||
if (auditStatus === 0 || auditStatus === null) {
|
||||
try {
|
||||
const response = await updateDocumentAuditStatus(fileId.toString(), 2);
|
||||
if (response.error) {
|
||||
console.error('更新文件审核状态失败:', response.error);
|
||||
alert('更新文件审核状态失败:' + (response.error || '未知错误'));
|
||||
// alert('更新文件审核状态失败:' + (response.error || '未知错误'));
|
||||
toastService.error('更新文件审核状态失败:' + (response.error || '未知错误'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新文件审核状态时出错:', error);
|
||||
alert('更新文件审核状态时出错:' + (error instanceof Error ? error.message : '未知错误'));
|
||||
// alert('更新文件审核状态时出错:' + (error instanceof Error ? error.message : '未知错误'));
|
||||
toastService.error('更新文件审核状态时出错:' + (error instanceof Error ? error.message : '未知错误'));
|
||||
}
|
||||
}
|
||||
|
||||
// 导航到评查详情页
|
||||
navigate(`/reviews?id=${fileId}`);
|
||||
navigate(`/reviews?id=${fileId}&previousRoute=documents`);
|
||||
};
|
||||
|
||||
// 使用useEffect监听fetcher状态变化并显示Toast
|
||||
useEffect(() => {
|
||||
if (fetcher.data && fetcher.state === 'idle') {
|
||||
if (fetcher.data.success) {
|
||||
toastService.success(fetcher.data.message);
|
||||
} else if (fetcher.data.message) {
|
||||
toastService.error(fetcher.data.message);
|
||||
}
|
||||
}
|
||||
}, [fetcher.data, fetcher.state]);
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
@@ -610,7 +635,7 @@ export default function DocumentsIndex() {
|
||||
const fileStatus = record.fileStatus || "-";
|
||||
const status = fileProcessingStatusOptions.find(s => s.value === fileStatus) ||
|
||||
fileProcessingStatusOptions[0];
|
||||
const isSpinning = fileStatus !== "Processed";
|
||||
const isSpinning = fileStatus !== "Processed" && fileStatus !== "Failed";
|
||||
return (
|
||||
<div className={`inline-flex items-center px-2 py-1 rounded-full text-xs bg-${status.color}-100 text-${status.color}-800`}>
|
||||
<i className={`${status.icon} ${isSpinning ? "animate-spin" : ""} mr-1`}></i>
|
||||
@@ -680,7 +705,7 @@ export default function DocumentsIndex() {
|
||||
</Link>
|
||||
) : (
|
||||
<Link
|
||||
to={`/reviews?id=${record.id}`}
|
||||
to={`/reviews?id=${record.id}&previousRoute=documents`}
|
||||
className="mr-1 hover:underline"
|
||||
>
|
||||
<i className="ri-eye-line"></i>
|
||||
|
||||
@@ -19,13 +19,14 @@ export const meta: MetaFunction = () => {
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
|
||||
// 文档审核状态定义
|
||||
enum DocumentAuditStatus {
|
||||
FAIL = -1,
|
||||
WAITING = 0,
|
||||
PASS = 1,
|
||||
WARNING = 2,
|
||||
PROCESSING = 3
|
||||
WAITING = 0,
|
||||
PROCESSING = 2
|
||||
}
|
||||
|
||||
// 文档状态对应的中文标签
|
||||
@@ -33,7 +34,6 @@ const STATUS_LABELS: Record<DocumentAuditStatus, string> = {
|
||||
[DocumentAuditStatus.FAIL]: "不通过",
|
||||
[DocumentAuditStatus.WAITING]: "待审核",
|
||||
[DocumentAuditStatus.PASS]: "通过",
|
||||
[DocumentAuditStatus.WARNING]: "警告",
|
||||
[DocumentAuditStatus.PROCESSING]: "审核中"
|
||||
};
|
||||
|
||||
@@ -314,7 +314,6 @@ export default function DocumentEdit() {
|
||||
<option value={DocumentAuditStatus.WAITING}>{STATUS_LABELS[DocumentAuditStatus.WAITING]}</option>
|
||||
<option value={DocumentAuditStatus.PROCESSING}>{STATUS_LABELS[DocumentAuditStatus.PROCESSING]}</option>
|
||||
<option value={DocumentAuditStatus.PASS}>{STATUS_LABELS[DocumentAuditStatus.PASS]}</option>
|
||||
<option value={DocumentAuditStatus.WARNING}>{STATUS_LABELS[DocumentAuditStatus.WARNING]}</option>
|
||||
<option value={DocumentAuditStatus.FAIL}>{STATUS_LABELS[DocumentAuditStatus.FAIL]}</option>
|
||||
</select>
|
||||
<div className="text-sm text-secondary mt-1">更改状态可能会影响此文档在列表中的显示和排序</div>
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
import { useState } from 'react';
|
||||
import { MessageModal, messageService, MessageModalProvider } from '~/components/ui/MessageModal';
|
||||
import type { MessageType } from '~/components/ui/MessageModal';
|
||||
import { LinksFunction } from '@remix-run/node';
|
||||
import messageModalStyles from '~/styles/components/message-modal.css?url';
|
||||
|
||||
export const links: LinksFunction = () => [
|
||||
{ rel: "stylesheet", href: messageModalStyles },
|
||||
];
|
||||
|
||||
export default function MessageModalExample() {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [modalType, setModalType] = useState<MessageType>('info');
|
||||
const [modalTitle, setModalTitle] = useState('');
|
||||
const [modalMessage, setModalMessage] = useState('');
|
||||
const [withConfirm, setWithConfirm] = useState(false);
|
||||
const [autoClose, setAutoClose] = useState(false);
|
||||
|
||||
// 打开普通模态框
|
||||
const openModal = (type: MessageType, title: string, message: string) => {
|
||||
setModalType(type);
|
||||
setModalTitle(title);
|
||||
setModalMessage(message);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
// 打开各类型的服务提示框
|
||||
const showSuccessMessage = () => {
|
||||
messageService.success('操作成功完成!', {
|
||||
title: '成功提示',
|
||||
autoClose: true
|
||||
});
|
||||
};
|
||||
|
||||
const showErrorMessage = () => {
|
||||
messageService.error('操作过程中发生错误,请重试。', {
|
||||
title: '错误提示'
|
||||
});
|
||||
};
|
||||
|
||||
const showWarningMessage = () => {
|
||||
messageService.warning('此操作可能产生不可逆转的结果。', {
|
||||
title: '警告提示',
|
||||
onConfirm: () => {
|
||||
messageService.success('您已确认继续操作')
|
||||
},
|
||||
confirmText: '继续操作',
|
||||
cancelText: '取消'
|
||||
});
|
||||
};
|
||||
|
||||
const showInfoMessage = () => {
|
||||
messageService.info('系统将于今晚10点进行升级维护。', {
|
||||
title: '通知',
|
||||
autoClose: true,
|
||||
autoCloseDelay: 5000
|
||||
});
|
||||
};
|
||||
|
||||
const showCustomMessage = () => {
|
||||
messageService.show({
|
||||
title: '自定义消息',
|
||||
message: '这是一个带有自定义内容的消息',
|
||||
type: 'info',
|
||||
confirmText: '了解',
|
||||
children: (
|
||||
<div style={{
|
||||
padding: '10px',
|
||||
backgroundColor: '#f0f0f0',
|
||||
borderRadius: '6px',
|
||||
marginTop: '10px'
|
||||
}}>
|
||||
<p>这里可以放置更复杂的自定义内容。</p>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginTop: '10px'
|
||||
}}>
|
||||
<i className="ri-information-line" style={{ marginRight: '8px', color: '#1890ff' }}></i>
|
||||
<span>比如详细的操作说明或其他信息。</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
});
|
||||
};
|
||||
|
||||
// 处理确认
|
||||
const handleConfirm = () => {
|
||||
messageService.success('您点击了确认按钮!', { autoClose: true });
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<MessageModalProvider>
|
||||
<div className="container mx-auto p-6">
|
||||
<h1 className="text-2xl font-bold mb-6">消息模态框示例</h1>
|
||||
|
||||
<div className="bg-white rounded-lg p-6 shadow-md mb-8">
|
||||
<h2 className="text-xl font-semibold mb-4">基本用法</h2>
|
||||
<p className="mb-4">您可以直接使用MessageModal组件来控制模态框的显示和隐藏。</p>
|
||||
|
||||
<div className="flex flex-wrap gap-3 mb-6">
|
||||
<button
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
onClick={() => openModal('info', '信息提示', '这是一个普通的信息提示框')}
|
||||
>
|
||||
信息提示框
|
||||
</button>
|
||||
<button
|
||||
className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600"
|
||||
onClick={() => openModal('success', '成功提示', '操作已成功完成')}
|
||||
>
|
||||
成功提示框
|
||||
</button>
|
||||
<button
|
||||
className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
|
||||
onClick={() => openModal('error', '错误提示', '操作失败,请检查输入')}
|
||||
>
|
||||
错误提示框
|
||||
</button>
|
||||
<button
|
||||
className="px-4 py-2 bg-yellow-500 text-white rounded hover:bg-yellow-600"
|
||||
onClick={() => openModal('warning', '警告提示', '此操作将删除数据,是否继续?')}
|
||||
>
|
||||
警告提示框
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-4 mb-4">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={withConfirm}
|
||||
onChange={(e) => setWithConfirm(e.target.checked)}
|
||||
className="mr-2"
|
||||
/>
|
||||
添加确认按钮
|
||||
</label>
|
||||
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoClose}
|
||||
onChange={(e) => setAutoClose(e.target.checked)}
|
||||
className="mr-2"
|
||||
/>
|
||||
自动关闭 (3秒)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<MessageModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
title={modalTitle}
|
||||
message={modalMessage}
|
||||
type={modalType}
|
||||
autoClose={autoClose}
|
||||
onConfirm={withConfirm ? handleConfirm : undefined}
|
||||
confirmText={withConfirm ? "确认" : "我知道了"}
|
||||
cancelText="取消"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg p-6 shadow-md mb-8">
|
||||
<h2 className="text-xl font-semibold mb-4">全局消息服务</h2>
|
||||
<p className="mb-4">使用messageService可以在任何组件中方便地显示消息提示,而无需手动管理状态。</p>
|
||||
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<button
|
||||
className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600"
|
||||
onClick={showSuccessMessage}
|
||||
>
|
||||
成功提示 (自动关闭)
|
||||
</button>
|
||||
<button
|
||||
className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
|
||||
onClick={showErrorMessage}
|
||||
>
|
||||
错误提示
|
||||
</button>
|
||||
<button
|
||||
className="px-4 py-2 bg-yellow-500 text-white rounded hover:bg-yellow-600"
|
||||
onClick={showWarningMessage}
|
||||
>
|
||||
警告提示 (带确认)
|
||||
</button>
|
||||
<button
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
onClick={showInfoMessage}
|
||||
>
|
||||
信息提示 (5秒后关闭)
|
||||
</button>
|
||||
<button
|
||||
className="px-4 py-2 bg-purple-500 text-white rounded hover:bg-purple-600"
|
||||
onClick={showCustomMessage}
|
||||
>
|
||||
自定义内容提示
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MessageModalProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import { useState } from 'react';
|
||||
import { Toast, toastService } from '~/components/ui/Toast';
|
||||
import type { ToastType } from '~/components/ui/Toast';
|
||||
import { LinksFunction } from '@remix-run/node';
|
||||
import toastStyles from '~/styles/components/toast.css?url';
|
||||
|
||||
export const links: LinksFunction = () => [
|
||||
{ rel: "stylesheet", href: toastStyles },
|
||||
];
|
||||
|
||||
export default function ToastExample() {
|
||||
const [isToastOpen, setIsToastOpen] = useState(false);
|
||||
const [toastType, setToastType] = useState<ToastType>('info');
|
||||
const [toastMessage, setToastMessage] = useState('');
|
||||
const [autoClose, setAutoClose] = useState(true);
|
||||
|
||||
// 打开普通通知
|
||||
const openToast = (type: ToastType, message: string) => {
|
||||
setToastType(type);
|
||||
setToastMessage(message);
|
||||
setIsToastOpen(true);
|
||||
};
|
||||
|
||||
// 使用服务显示不同类型通知
|
||||
const showSuccessToast = () => {
|
||||
toastService.success('操作成功完成!');
|
||||
};
|
||||
|
||||
const showErrorToast = () => {
|
||||
toastService.error('操作过程中发生错误,请重试。');
|
||||
};
|
||||
|
||||
const showWarningToast = () => {
|
||||
toastService.warning('此操作可能产生不可逆转的结果。');
|
||||
};
|
||||
|
||||
const showInfoToast = () => {
|
||||
toastService.info('系统将于今晚10点进行升级维护。');
|
||||
};
|
||||
|
||||
// 显示多行文本的长通知
|
||||
const showLongToast = () => {
|
||||
toastService.info('这是一个具有很长内容的通知,将自动换行以适应容器宽度,并且最多显示三行,超出部分会被截断。系统会自动处理长文本的换行和截断,确保显示效果一致。');
|
||||
};
|
||||
|
||||
// 短时间内显示多个通知
|
||||
const showMultipleToasts = () => {
|
||||
toastService.success('第一条通知');
|
||||
setTimeout(() => {
|
||||
toastService.info('第二条通知');
|
||||
}, 300);
|
||||
setTimeout(() => {
|
||||
toastService.warning('第三条通知');
|
||||
}, 600);
|
||||
setTimeout(() => {
|
||||
toastService.error('第四条通知');
|
||||
}, 900);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-6">
|
||||
<h1 className="text-2xl font-bold mb-6">轻量级顶部通知示例</h1>
|
||||
|
||||
<div className="bg-white rounded-lg p-6 shadow-md mb-8">
|
||||
<h2 className="text-xl font-semibold mb-4">基本用法</h2>
|
||||
<p className="mb-4">您可以直接使用Toast组件来控制通知的显示和隐藏。</p>
|
||||
|
||||
<div className="flex flex-wrap gap-3 mb-6">
|
||||
<button
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
onClick={() => openToast('info', '这是一个信息通知')}
|
||||
>
|
||||
信息通知
|
||||
</button>
|
||||
<button
|
||||
className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600"
|
||||
onClick={() => openToast('success', '操作已成功完成')}
|
||||
>
|
||||
成功通知
|
||||
</button>
|
||||
<button
|
||||
className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
|
||||
onClick={() => openToast('error', '操作失败,请检查输入')}
|
||||
>
|
||||
错误通知
|
||||
</button>
|
||||
<button
|
||||
className="px-4 py-2 bg-yellow-500 text-white rounded hover:bg-yellow-600"
|
||||
onClick={() => openToast('warning', '请注意,这是一个警告通知')}
|
||||
>
|
||||
警告通知
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoClose}
|
||||
onChange={(e) => setAutoClose(e.target.checked)}
|
||||
className="mr-2"
|
||||
/>
|
||||
自动关闭 (3秒)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<Toast
|
||||
isOpen={isToastOpen}
|
||||
onClose={() => setIsToastOpen(false)}
|
||||
message={toastMessage}
|
||||
type={toastType}
|
||||
autoClose={autoClose}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg p-6 shadow-md mb-8">
|
||||
<h2 className="text-xl font-semibold mb-4">全局通知服务</h2>
|
||||
<p className="mb-4">使用toastService可以在任何组件中方便地显示通知,而无需手动管理状态。</p>
|
||||
|
||||
<div className="flex flex-wrap gap-3 mb-6">
|
||||
<button
|
||||
className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600"
|
||||
onClick={showSuccessToast}
|
||||
>
|
||||
成功通知
|
||||
</button>
|
||||
<button
|
||||
className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
|
||||
onClick={showErrorToast}
|
||||
>
|
||||
错误通知
|
||||
</button>
|
||||
<button
|
||||
className="px-4 py-2 bg-yellow-500 text-white rounded hover:bg-yellow-600"
|
||||
onClick={showWarningToast}
|
||||
>
|
||||
警告通知
|
||||
</button>
|
||||
<button
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
onClick={showInfoToast}
|
||||
>
|
||||
信息通知
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<button
|
||||
className="px-4 py-2 bg-purple-500 text-white rounded hover:bg-purple-600"
|
||||
onClick={showLongToast}
|
||||
>
|
||||
长文本通知
|
||||
</button>
|
||||
<button
|
||||
className="px-4 py-2 bg-pink-500 text-white rounded hover:bg-pink-600"
|
||||
onClick={showMultipleToasts}
|
||||
>
|
||||
多个通知
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
type FileUploadResponse,
|
||||
DocumentStatus
|
||||
} from "~/api/files/files-upload";
|
||||
import { updateDocumentAuditStatus } from "~/api/evaluation_points/rules-files";
|
||||
|
||||
export function links() {
|
||||
return [
|
||||
@@ -781,6 +782,24 @@ export default function FilesUpload() {
|
||||
const type = documentTypesState.find(t => t.id === codeId);
|
||||
return type ? type.name : '未知类型';
|
||||
};
|
||||
|
||||
// 处理查看文件
|
||||
const handleViewFile = async (record: Document) => {
|
||||
// 检查audit_status是否为0,如果是则更新为2
|
||||
if (record.audit_status === 0 || record.audit_status === null) {
|
||||
try {
|
||||
const response = await updateDocumentAuditStatus(record.id.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=${record.id}&previousRoute=filesUpload`);
|
||||
};
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
@@ -880,7 +899,7 @@ export default function FilesUpload() {
|
||||
size="small"
|
||||
disabled={record.status !== DocumentStatus.PROCESSED}
|
||||
icon="ri-eye-line"
|
||||
onClick={() => navigate(`/reviews?id=${record.id}`)}
|
||||
onClick={() => handleViewFile(record)}
|
||||
>
|
||||
查看
|
||||
</Button>
|
||||
|
||||
+81
-7
@@ -29,7 +29,7 @@ import { type MetaFunction, type LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNavigate, useLoaderData } from "@remix-run/react";
|
||||
import reviewsStyles from "~/styles/reviews.css?url";
|
||||
import { getReviewPoints, updateReviewResult } from "~/api/evaluation_points/reviews";
|
||||
import { getReviewPoints, updateReviewResult, confirmReviewResults } from "~/api/evaluation_points/reviews";
|
||||
|
||||
// 导入评查详情页面组件
|
||||
import {
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
// 从ReviewPointsList组件中导入ReviewPoint类型
|
||||
import { type ReviewPoint } from '~/components/reviews';
|
||||
|
||||
|
||||
/**
|
||||
* 文件信息组件
|
||||
* 显示文件名称、状态信息以及操作按钮(下载原文件、导出评查报告、确认评查结果)
|
||||
@@ -148,6 +149,9 @@ interface ReviewData {
|
||||
aiAnalysis: AnalysisData;
|
||||
}
|
||||
|
||||
interface LoaderData {
|
||||
previousRoute: string;
|
||||
}
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ title: "评查详情 - 中国烟草AI合同及卷宗审核系统" },
|
||||
@@ -163,13 +167,38 @@ export function links() {
|
||||
}
|
||||
|
||||
export const handle = {
|
||||
breadcrumb: "评查详情"
|
||||
breadcrumb: "评查详情",
|
||||
// 添加一个previousRoute属性用于支持自定义的面包屑导航
|
||||
previousRoute: (data:LoaderData)=>{
|
||||
if(data.previousRoute){
|
||||
if(data.previousRoute === 'filesUpload'){
|
||||
return {
|
||||
title: "文件上传",
|
||||
to: "/files/upload"
|
||||
}
|
||||
}
|
||||
if(data.previousRoute === 'documents'){
|
||||
return {
|
||||
title: "文档列表",
|
||||
to: "/documents"
|
||||
}
|
||||
}
|
||||
if(data.previousRoute === 'rulesFiles'){
|
||||
return {
|
||||
title: "评查文件列表",
|
||||
to: "/rules-files"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
const id = url.searchParams.get('id') || undefined;
|
||||
const previousRoute = url.searchParams.get('previousRoute') || '';
|
||||
// console.log("id-------",id);
|
||||
if (!id) {
|
||||
return Response.json({ error: '评查ID不能为空' }, { status: 400 });
|
||||
@@ -188,6 +217,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
// 确保reviewData有效且具有预期的属性
|
||||
if ('document' in reviewData && 'data' in reviewData && 'reviewInfo' in reviewData && 'stats' in reviewData) {
|
||||
return Response.json({
|
||||
previousRoute: previousRoute,
|
||||
document: reviewData.document,
|
||||
reviewPoints: reviewData.data,
|
||||
reviewInfo: reviewData.reviewInfo,
|
||||
@@ -219,6 +249,7 @@ export default function ReviewDetails() {
|
||||
// 构建文件信息对象
|
||||
const fileInfo = {
|
||||
fileName: document.name || "未知文件名",
|
||||
path: document.path || "未知路径",
|
||||
contractNumber: document.documentNumber || "未知编号",
|
||||
fileSize: document.size ? formatFileSize(document.size) : "未知大小",
|
||||
fileFormat: document.fileType ? document.fileType.toUpperCase() : "未知格式",
|
||||
@@ -252,8 +283,19 @@ export default function ReviewDetails() {
|
||||
};
|
||||
|
||||
const handleReviewPointSelect = (reviewPointId: string, page?: number) => {
|
||||
setActiveReviewPointId(reviewPointId);
|
||||
setTargetPage(page);
|
||||
// 如果点击的是相同的评查点,但有page参数,先重置targetPage以确保useEffect能够触发
|
||||
if (reviewPointId === activeReviewPointId && page) {
|
||||
setTargetPage(undefined);
|
||||
// 使用setTimeout确保状态更新后再设置新的targetPage
|
||||
setTimeout(() => {
|
||||
setActiveReviewPointId(reviewPointId);
|
||||
setTargetPage(page);
|
||||
}, 0);
|
||||
} else {
|
||||
// 正常设置activeReviewPointId和targetPage
|
||||
setActiveReviewPointId(reviewPointId);
|
||||
setTargetPage(page);
|
||||
}
|
||||
};
|
||||
|
||||
// 刷新评审数据
|
||||
@@ -378,9 +420,36 @@ export default function ReviewDetails() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmResults = () => {
|
||||
alert('评查结果已确认');
|
||||
navigate('/reviews'); // 假设评查列表页面路径为 /reviews
|
||||
const handleConfirmResults = async () => {
|
||||
if (!document || !document.id) {
|
||||
alert('文档数据不完整,无法确认评查结果');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 显示加载状态
|
||||
setIsLoading(true);
|
||||
|
||||
// 调用API确认评查结果
|
||||
const response = await confirmReviewResults(document.id.toString());
|
||||
|
||||
if (response.error) {
|
||||
console.error('确认评查结果失败:', response.error);
|
||||
alert(`确认评查结果失败: ${response.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示成功消息
|
||||
alert('评查结果已确认,文档审核状态已更新');
|
||||
|
||||
// 导航到文档列表页
|
||||
navigate('/documents');
|
||||
} catch (error) {
|
||||
console.error('确认评查结果出错:', error);
|
||||
alert(`确认评查结果失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -529,6 +598,7 @@ function getMockReviewData(): ReviewData {
|
||||
reviewPoints: [
|
||||
{
|
||||
id: "1",
|
||||
pointName: "付款条款",
|
||||
title: "付款条件描述不明确",
|
||||
groupName: "付款条款清晰性",
|
||||
// location: "交货与付款条款",
|
||||
@@ -540,6 +610,7 @@ function getMockReviewData(): ReviewData {
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
pointName: "违约责任",
|
||||
title: "违约责任条款缺失",
|
||||
groupName: "合同权利义务对等性",
|
||||
status: "warning",
|
||||
@@ -550,6 +621,7 @@ function getMockReviewData(): ReviewData {
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
pointName: "签章审核",
|
||||
title: "签章不完整",
|
||||
groupName: "合同签署规范性",
|
||||
status: "warning",
|
||||
@@ -562,6 +634,7 @@ function getMockReviewData(): ReviewData {
|
||||
},
|
||||
{
|
||||
id: "9",
|
||||
pointName: "交货方式",
|
||||
title: "交货方式描述模糊",
|
||||
groupName: "履行条款明确性",
|
||||
status: "success",
|
||||
@@ -576,6 +649,7 @@ function getMockReviewData(): ReviewData {
|
||||
},
|
||||
{
|
||||
id: "10",
|
||||
pointName: "法律适用",
|
||||
title: "法律适用条款缺失",
|
||||
groupName: "争议解决条款完整性",
|
||||
status: "error",
|
||||
|
||||
@@ -542,7 +542,12 @@ export default function RuleGroupsIndex() {
|
||||
<div className="content-container rule-groups-page">
|
||||
{/* 页面头部 */}
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-xl font-medium">评查点分组管理</h2>
|
||||
<h2 className="text-xl font-medium flex">评查点分组管理
|
||||
<div className="flex items-center bg-white px-3 py-1 rounded-md">
|
||||
<span className="text-sm text-secondary">分组数:</span>
|
||||
<span className="text-base font-normal text-primary ml-1">{processedData.length}</span>
|
||||
</div>
|
||||
</h2>
|
||||
<div className="flex">
|
||||
<Button
|
||||
type="default"
|
||||
@@ -634,13 +639,13 @@ export default function RuleGroupsIndex() {
|
||||
/>
|
||||
|
||||
{/* 分页 - 使用Pagination组件 */}
|
||||
<Pagination
|
||||
{/* <Pagination
|
||||
currentPage={1}
|
||||
total={processedData.length}
|
||||
pageSize={10}
|
||||
onChange={() => {}}
|
||||
showTotal={true}
|
||||
/>
|
||||
/> */}
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
@@ -167,7 +167,7 @@ export default function RulesFiles() {
|
||||
// 查看评查文件
|
||||
const handleReviewFileClick = async (fileId: string, auditStatus: number | null) => {
|
||||
// 检查audit_status是否为0,如果是则更新为2
|
||||
if (auditStatus === 0) {
|
||||
if (auditStatus === 0 || auditStatus === null) {
|
||||
try {
|
||||
const response = await updateDocumentAuditStatus(fileId, 2);
|
||||
if (response.error) {
|
||||
@@ -181,7 +181,7 @@ export default function RulesFiles() {
|
||||
}
|
||||
|
||||
// 导航到评查详情页
|
||||
navigate(`/reviews?id=${fileId}`);
|
||||
navigate(`/reviews?id=${fileId}&previousRoute=rulesFiles`);
|
||||
};
|
||||
|
||||
// 渲染问题摘要
|
||||
|
||||
+90
-17
@@ -49,6 +49,7 @@ import type { EvaluationPointGroup } from "~/models/evaluation_point_groups";
|
||||
// 导入RuleContext上下文
|
||||
import { RuleContext } from "~/contexts/RuleContext";
|
||||
import { postgrestGet, postgrestPost, postgrestPut } from "~/api/postgrest-client";
|
||||
import { toastService } from '~/components/ui/Toast';
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
@@ -65,7 +66,21 @@ export function links() {
|
||||
}
|
||||
|
||||
export const handle = {
|
||||
breadcrumb: "评查点管理"
|
||||
breadcrumb: "评查点管理",
|
||||
previousRoute: () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const mode = searchParams.get('mode');
|
||||
const id = searchParams.get('id');
|
||||
if (mode || id) {
|
||||
return {
|
||||
title: "评查点列表",
|
||||
to: `/rules`
|
||||
};
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
// 添加规则配置接口
|
||||
@@ -266,13 +281,13 @@ export default function RuleNew() {
|
||||
setInstanceKey(`edit_${id}_${Date.now()}`);
|
||||
} catch (jsonError) {
|
||||
console.error('JSON处理错误:', jsonError);
|
||||
alert(`数据处理错误: ${jsonError instanceof Error ? jsonError.message : '未知错误'}`);
|
||||
toastService.error(`数据处理错误: ${jsonError instanceof Error ? jsonError.message : '未知错误'}`);
|
||||
resetFormData();
|
||||
navigate('/rules');
|
||||
}
|
||||
} else {
|
||||
console.error('获取数据失败: 返回数据为空');
|
||||
alert('获取数据失败: 返回数据为空');
|
||||
toastService.error('获取数据失败: 返回数据为空');
|
||||
resetFormData();
|
||||
navigate('/rules');
|
||||
}
|
||||
@@ -281,7 +296,7 @@ export default function RuleNew() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取评查点数据失败:', error);
|
||||
alert(`获取评查点数据失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
toastService.error(`获取评查点数据失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
// 获取数据失败时返回上一页
|
||||
resetFormData();
|
||||
navigate('/rules');
|
||||
@@ -305,7 +320,7 @@ export default function RuleNew() {
|
||||
} catch (error) {
|
||||
console.error('获取评查点组数据失败:', error);
|
||||
// 显示错误提示但不影响应用继续使用
|
||||
alert(`获取评查点组数据失败: ${error instanceof Error ? error.message : '未知错误'}\n将使用默认数据`);
|
||||
toastService.error(`获取评查点组数据失败: ${error instanceof Error ? error.message : '未知错误'}\n将使用默认数据`);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -314,12 +329,12 @@ export default function RuleNew() {
|
||||
|
||||
// 验证必填字段
|
||||
if (!formData.name?.trim()) {
|
||||
alert("评查点名称不能为空");
|
||||
toastService.warning("评查点名称不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.code?.trim()) {
|
||||
alert("评查点编码不能为空");
|
||||
toastService.warning("评查点编码不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -379,6 +394,23 @@ export default function RuleNew() {
|
||||
score: formData.score !== undefined ? Number(formData.score) : 0
|
||||
};
|
||||
|
||||
// 获取当前所有有效的抽取字段
|
||||
const currentExtractionFields = extractionFields.map(field => {
|
||||
// 处理字段名,去掉类型后缀(如果有)
|
||||
if (field.includes('_')) {
|
||||
return field.split('_')[0];
|
||||
}
|
||||
return field;
|
||||
});
|
||||
|
||||
// 去重,确保不会有重复字段
|
||||
const validFields = [...new Set(currentExtractionFields)];
|
||||
console.log("当前有效的抽取字段:", validFields);
|
||||
|
||||
// 重要:这段代码解决了字段删除后,评查配置中仍保留已删除字段的问题
|
||||
// 在保存前,我们会确保所有规则中引用的字段都是当前有效的抽取字段
|
||||
// 这样即使用户在界面操作中删除了某个抽取字段,相关的评查规则也会被自动清理
|
||||
|
||||
// 确保rules中的每个配置对象都被正确处理
|
||||
if (cleanedData.evaluation_config && Array.isArray(cleanedData.evaluation_config.rules)) {
|
||||
cleanedData.evaluation_config.rules = cleanedData.evaluation_config.rules
|
||||
@@ -390,6 +422,12 @@ export default function RuleNew() {
|
||||
switch (rule.type) {
|
||||
case 'exists':
|
||||
if (!Array.isArray((config as ExistsRuleConfig).fields)) (config as ExistsRuleConfig).fields = [];
|
||||
// 过滤掉不存在于当前抽取字段中的字段
|
||||
if (Array.isArray((config as ExistsRuleConfig).fields)) {
|
||||
(config as ExistsRuleConfig).fields = (config as ExistsRuleConfig).fields.filter(
|
||||
field => validFields.includes(field)
|
||||
);
|
||||
}
|
||||
if (!(config as ExistsRuleConfig).logic) (config as ExistsRuleConfig).logic = 'and';
|
||||
// 删除不必要的字段
|
||||
delete (config as ExistsRuleConfig & {availableFields?: string}).availableFields;
|
||||
@@ -399,6 +437,12 @@ export default function RuleNew() {
|
||||
|
||||
case 'consistency':
|
||||
if (!Array.isArray((config as ConsistencyRuleConfig).pairs)) (config as ConsistencyRuleConfig).pairs = [];
|
||||
// 过滤掉包含不存在于当前抽取字段中的字段的配对
|
||||
if (Array.isArray((config as ConsistencyRuleConfig).pairs)) {
|
||||
(config as ConsistencyRuleConfig).pairs = (config as ConsistencyRuleConfig).pairs.filter(
|
||||
pair => validFields.includes(pair.sourceField) && validFields.includes(pair.targetField)
|
||||
);
|
||||
}
|
||||
if (!(config as ConsistencyRuleConfig).logic) (config as ConsistencyRuleConfig).logic = 'and';
|
||||
delete (config as ConsistencyRuleConfig & {availableFields?: string}).availableFields;
|
||||
delete (config as ConsistencyRuleConfig).logicRelation;
|
||||
@@ -408,6 +452,10 @@ export default function RuleNew() {
|
||||
break;
|
||||
|
||||
case 'format':
|
||||
// 检查字段是否存在于当前抽取字段中
|
||||
if ((config as FormatRuleConfig).field && !validFields.includes((config as FormatRuleConfig).field)) {
|
||||
(config as FormatRuleConfig).field = '';
|
||||
}
|
||||
if (!(config as FormatRuleConfig).field) (config as FormatRuleConfig).field = '';
|
||||
if (!(config as FormatRuleConfig).formatType) (config as FormatRuleConfig).formatType = 'date';
|
||||
if (!(config as FormatRuleConfig).parameters) (config as FormatRuleConfig).parameters = '';
|
||||
@@ -418,6 +466,12 @@ export default function RuleNew() {
|
||||
|
||||
case 'logic':
|
||||
if (!Array.isArray((config as LogicRuleConfig).conditions)) (config as LogicRuleConfig).conditions = [];
|
||||
// 过滤掉包含不存在于当前抽取字段中的字段的条件
|
||||
if (Array.isArray((config as LogicRuleConfig).conditions)) {
|
||||
(config as LogicRuleConfig).conditions = (config as LogicRuleConfig).conditions.filter(
|
||||
condition => validFields.includes(condition.field)
|
||||
);
|
||||
}
|
||||
if (!(config as LogicRuleConfig).logic) (config as LogicRuleConfig).logic = 'and';
|
||||
delete (config as LogicRuleConfig & {availableFields?: string}).availableFields;
|
||||
delete (config as LogicRuleConfig).logicRelation;
|
||||
@@ -427,6 +481,10 @@ export default function RuleNew() {
|
||||
break;
|
||||
|
||||
case 'regex':
|
||||
// 检查字段是否存在于当前抽取字段中
|
||||
if ((config as RegexRuleConfig).field && !validFields.includes((config as RegexRuleConfig).field)) {
|
||||
(config as RegexRuleConfig).field = '';
|
||||
}
|
||||
if (!(config as RegexRuleConfig).field) (config as RegexRuleConfig).field = '';
|
||||
if (!(config as RegexRuleConfig).pattern) (config as RegexRuleConfig).pattern = '';
|
||||
if (!(config as RegexRuleConfig).matchType) (config as RegexRuleConfig).matchType = 'match';
|
||||
@@ -456,6 +514,8 @@ export default function RuleNew() {
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// console.log("当前评查配置-----------------:", formData.evaluation_config);
|
||||
|
||||
// 如果是新建模式,则删除id字段
|
||||
if (!isEditMode) {
|
||||
@@ -499,14 +559,19 @@ export default function RuleNew() {
|
||||
}
|
||||
|
||||
if (response.error) {
|
||||
alert(`系统繁忙: ${response.error}`);
|
||||
if (response.error.includes('evaluation_points_code_key')) {
|
||||
toastService.error('在基本信息中:评查点编码已存在,请修改后保存。');
|
||||
} else {
|
||||
toastService.error(`系统繁忙: ${response.error}`);
|
||||
}
|
||||
setIsLoading(false);
|
||||
} else if (response.data && Array.isArray(response.data) && response.data.length > 0) {
|
||||
// 获取新创建或更新的评查点ID
|
||||
const savedPointId = response.data[0]?.id;
|
||||
|
||||
if (savedPointId) {
|
||||
// 显示成功消息
|
||||
alert(`评查点${isEditMode ? '更新' : '创建'}成功!`);
|
||||
toastService.success(`评查点${isEditMode ? '更新' : '创建'}成功!`);
|
||||
|
||||
// 保存成功后跳转到编辑页面并重新加载数据
|
||||
navigate(`/rules-new?id=${savedPointId}`, { replace: true });
|
||||
@@ -514,22 +579,22 @@ export default function RuleNew() {
|
||||
await fetchEvaluationPoint(savedPointId);
|
||||
} else {
|
||||
// 无法获取ID的情况
|
||||
alert(`评查点${isEditMode ? '更新' : '创建'}成功,但无法获取ID。正在返回列表页面。`);
|
||||
toastService.warning(`评查点${isEditMode ? '更新' : '创建'}成功,但无法获取ID。正在返回列表页面。`);
|
||||
navigate('/rules');
|
||||
}
|
||||
} else {
|
||||
alert(`系统繁忙`);
|
||||
toastService.error('系统繁忙');
|
||||
}
|
||||
} catch (jsonError) {
|
||||
console.error("JSON处理错误:", jsonError);
|
||||
alert(`数据处理错误: ${jsonError instanceof Error ? jsonError.message : '未知错误'}`);
|
||||
toastService.error(`数据处理错误: ${jsonError instanceof Error ? jsonError.message : '未知错误'}`);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("数据处理错误:", error);
|
||||
alert(`数据处理错误: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
toastService.error(`数据处理错误: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
@@ -586,17 +651,21 @@ export default function RuleNew() {
|
||||
customLogic: '',
|
||||
rules: []
|
||||
};
|
||||
|
||||
// console.log("当前评查配置:", currentConfig);
|
||||
// console.log("变更评查配置:", data.evaluation_config);
|
||||
|
||||
// 合并评查配置数据
|
||||
const mergedConfig = {
|
||||
...currentConfig,
|
||||
...(data.evaluation_config as object)
|
||||
};
|
||||
|
||||
// console.log("合并评查配置:", data.evaluation_config);
|
||||
// 更新表单数据
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
evaluation_config: mergedConfig
|
||||
// evaluation_config: data.evaluation_config as typeof prev.evaluation_config
|
||||
}));
|
||||
} else {
|
||||
// 处理其他普通字段
|
||||
@@ -634,11 +703,15 @@ export default function RuleNew() {
|
||||
*/
|
||||
useEffect(() => {
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
const id = searchParams.get('id');
|
||||
const id = searchParams.get('id');
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
// 设置编辑模式
|
||||
const newIsEditMode = !!id;
|
||||
setIsEditMode(newIsEditMode);
|
||||
if (mode && mode === 'copy') {
|
||||
setIsEditMode(false);
|
||||
} else {
|
||||
setIsEditMode(!!id);
|
||||
}
|
||||
|
||||
if (id) {
|
||||
// 编辑模式:获取数据
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { type MetaFunction, type LoaderFunctionArgs, redirect } from "@remix-run/node";
|
||||
import { useLoaderData, useSearchParams, useSubmit, Link } from "@remix-run/react";
|
||||
import { useLoaderData, useSearchParams, useSubmit, Link, useNavigate } from "@remix-run/react";
|
||||
import { Button } from '~/components/ui/Button';
|
||||
import { Card } from '~/components/ui/Card';
|
||||
import { Tag } from '~/components/ui/Tag';
|
||||
@@ -234,6 +234,7 @@ export default function RulesIndex() {
|
||||
const ruleTypes = loaderData.ruleTypes || []; // 添加默认空数组避免undefined
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const submit = useSubmit();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 状态管理
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
@@ -345,11 +346,12 @@ export default function RulesIndex() {
|
||||
};
|
||||
|
||||
const handleCopy = (rule: Rule) => {
|
||||
const formData = new FormData();
|
||||
formData.append('_action', 'duplicate');
|
||||
formData.append('ruleId', rule.id);
|
||||
// const formData = new FormData();
|
||||
// formData.append('_action', 'duplicate');
|
||||
// formData.append('ruleId', rule.id);
|
||||
|
||||
submit(formData, { method: 'post' });
|
||||
// submit(formData, { method: 'post' });
|
||||
navigate(`/rules-new?id=${rule.id}&mode=copy`);
|
||||
};
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
/*
|
||||
* 消息模态框样式
|
||||
* 支持成功、错误、警告、信息四种类型的提示消息
|
||||
* 包含动画效果和响应式布局
|
||||
*/
|
||||
|
||||
/* 模态框遮罩层 */
|
||||
.message-modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
animation: fadeIn 0.3s ease forwards;
|
||||
}
|
||||
|
||||
.message-modal-overlay.closing {
|
||||
animation: fadeOut 0.3s ease forwards;
|
||||
}
|
||||
|
||||
/* 模态框容器 */
|
||||
.message-modal {
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15);
|
||||
padding: 24px;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
animation: slideUp 0.3s ease forwards;
|
||||
}
|
||||
|
||||
.message-modal.closing {
|
||||
animation: slideDown 0.3s ease forwards;
|
||||
}
|
||||
|
||||
/* 关闭按钮 */
|
||||
.message-modal-close {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
color: #666;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.message-modal-close:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.message-modal-close i {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* 图标包装器 */
|
||||
.message-modal-icon-wrapper {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 消息图标 */
|
||||
.message-modal-icon {
|
||||
font-size: 48px;
|
||||
height: 64px;
|
||||
width: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* 图标类型样式 */
|
||||
.message-modal-icon.success {
|
||||
color: #52c41a;
|
||||
background-color: rgba(82, 196, 26, 0.1);
|
||||
}
|
||||
|
||||
.message-modal-icon.error {
|
||||
color: #f5222d;
|
||||
background-color: rgba(245, 34, 45, 0.1);
|
||||
}
|
||||
|
||||
.message-modal-icon.warning {
|
||||
color: #faad14;
|
||||
background-color: rgba(250, 173, 20, 0.1);
|
||||
}
|
||||
|
||||
.message-modal-icon.info {
|
||||
color: #1890ff;
|
||||
background-color: rgba(24, 144, 255, 0.1);
|
||||
}
|
||||
|
||||
/* 模态框内容 */
|
||||
.message-modal-content {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* 模态框标题 */
|
||||
.message-modal-title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 模态框消息 */
|
||||
.message-modal-message {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* 自定义内容 */
|
||||
.message-modal-custom-content {
|
||||
margin-top: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 按钮容器 */
|
||||
.message-modal-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.message-modal-button {
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #d9d9d9;
|
||||
background: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.message-modal-button:hover {
|
||||
border-color: #1890ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.message-modal-button.primary {
|
||||
background-color: #1890ff;
|
||||
color: white;
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
.message-modal-button.primary:hover {
|
||||
background-color: #40a9ff;
|
||||
border-color: #40a9ff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 模态框类型特定样式 */
|
||||
.message-modal-success .message-modal-button.primary {
|
||||
background-color: #52c41a;
|
||||
border-color: #52c41a;
|
||||
}
|
||||
|
||||
.message-modal-success .message-modal-button.primary:hover {
|
||||
background-color: #73d13d;
|
||||
border-color: #73d13d;
|
||||
}
|
||||
|
||||
.message-modal-error .message-modal-button.primary {
|
||||
background-color: #f5222d;
|
||||
border-color: #f5222d;
|
||||
}
|
||||
|
||||
.message-modal-error .message-modal-button.primary:hover {
|
||||
background-color: #ff4d4f;
|
||||
border-color: #ff4d4f;
|
||||
}
|
||||
|
||||
.message-modal-warning .message-modal-button.primary {
|
||||
background-color: #faad14;
|
||||
border-color: #faad14;
|
||||
}
|
||||
|
||||
.message-modal-warning .message-modal-button.primary:hover {
|
||||
background-color: #ffc53d;
|
||||
border-color: #ffc53d;
|
||||
}
|
||||
|
||||
/* 动画关键帧 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式样式 */
|
||||
@media (max-width: 480px) {
|
||||
.message-modal {
|
||||
max-width: 90%;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.message-modal-icon {
|
||||
font-size: 36px;
|
||||
height: 52px;
|
||||
width: 52px;
|
||||
}
|
||||
|
||||
.message-modal-title {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.message-modal-message {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.message-modal-actions {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.message-modal-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
/*
|
||||
* 轻量级顶部通知样式
|
||||
* 在页面顶部显示简洁的通知横条
|
||||
*/
|
||||
|
||||
/* 通知容器 */
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
max-width: 800px; /* 增加最大宽度,但保持在移动设备上的响应式 */
|
||||
padding: 0 20px;
|
||||
box-sizing: border-box;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 通知条样式 */
|
||||
.toast {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
align-items: flex-start; /* 改为flex-start让图标和文本从顶部对齐,更好地支持多行文本 */
|
||||
justify-content: space-between;
|
||||
width: fit-content; /* 更好地适应内容宽度 */
|
||||
min-width: 300px; /* 最小宽度 */
|
||||
max-width: 100%; /* 相对于容器的最大宽度 */
|
||||
animation: slideInDown 0.3s ease forwards;
|
||||
overflow: hidden;
|
||||
pointer-events: auto;
|
||||
border-left: 4px solid #1890ff;
|
||||
margin: 0 auto; /* 水平居中 */
|
||||
transition: width 0.2s ease-out; /* 添加平滑的宽度过渡效果 */
|
||||
}
|
||||
|
||||
/* 多行文本的Toast样式调整 */
|
||||
.toast.toast-multiline {
|
||||
padding-top: 14px;
|
||||
padding-bottom: 14px;
|
||||
}
|
||||
|
||||
.toast.closing {
|
||||
animation: slideOutUp 0.3s ease forwards;
|
||||
}
|
||||
|
||||
.toast-content {
|
||||
display: flex;
|
||||
/* align-items: flex-start; 改为flex-start让图标和文本从顶部对齐 */
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0; /* 确保flex子项不会溢出父容器 */
|
||||
margin-right: 8px; /* 给关闭按钮留出空间 */
|
||||
}
|
||||
|
||||
/* 图标容器 */
|
||||
.toast-icon-wrapper {
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px; /* 微调图标位置,与文本第一行更好地对齐 */
|
||||
}
|
||||
|
||||
/* 通知图标 */
|
||||
.toast-icon {
|
||||
font-size: 20px;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 图标类型样式 */
|
||||
.toast-icon.success {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.toast-icon.error {
|
||||
color: #f5222d;
|
||||
}
|
||||
|
||||
.toast-icon.warning {
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
.toast-icon.info {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
/* 通知类型样式 */
|
||||
.toast-success {
|
||||
border-left-color: #52c41a;
|
||||
}
|
||||
|
||||
.toast-error {
|
||||
border-left-color: #f5222d;
|
||||
}
|
||||
|
||||
.toast-warning {
|
||||
border-left-color: #faad14;
|
||||
}
|
||||
|
||||
.toast-info {
|
||||
border-left-color: #1890ff;
|
||||
}
|
||||
|
||||
/* 消息样式 */
|
||||
.toast-message {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: #333;
|
||||
word-break: break-word; /* 允许在任何字符处换行 */
|
||||
word-wrap: break-word; /* 长词自动换行 */
|
||||
white-space: pre-wrap; /* 保留空格和换行,但允许文本换行 */
|
||||
overflow-wrap: break-word; /* 确保长单词也能换行 */
|
||||
flex: 1;
|
||||
min-width: 0; /* 确保flex子项不会溢出父容器 */
|
||||
max-height: none; /* 移除最大高度限制,让内容自然增长 */
|
||||
max-width: calc(100% - 60px); /* 确保有足够空间给图标和关闭按钮 */
|
||||
}
|
||||
|
||||
/* 关闭按钮 */
|
||||
.toast-close {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
color: #999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
margin-left: 8px;
|
||||
flex-shrink: 0;
|
||||
padding: 0;
|
||||
align-self: flex-start; /* 按钮始终位于顶部 */
|
||||
margin-top: 2px; /* 微调按钮位置 */
|
||||
}
|
||||
|
||||
.toast-close:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.toast-close i {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Toast焦点状态样式 */
|
||||
.toast:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
|
||||
.toast-success:focus {
|
||||
box-shadow: 0 0 0 3px rgba(82, 196, 26, 0.2);
|
||||
}
|
||||
|
||||
.toast-error:focus {
|
||||
box-shadow: 0 0 0 3px rgba(245, 34, 45, 0.2);
|
||||
}
|
||||
|
||||
.toast-warning:focus {
|
||||
box-shadow: 0 0 0 3px rgba(250, 173, 20, 0.2);
|
||||
}
|
||||
|
||||
/* 动画关键帧 */
|
||||
@keyframes slideInDown {
|
||||
from {
|
||||
transform: translateY(-20px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOutUp {
|
||||
from {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateY(-20px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式样式 */
|
||||
@media (max-width: 768px) {
|
||||
.toast-container {
|
||||
padding: 0 15px;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.toast {
|
||||
min-width: 250px;
|
||||
width: auto; /* 在平板上自动调整宽度 */
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.toast-container {
|
||||
top: 10px;
|
||||
padding: 0 10px;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.toast {
|
||||
padding: 10px 12px;
|
||||
min-width: 0; /* 移动设备上取消最小宽度限制 */
|
||||
width: 100%; /* 在移动设备上占满容器宽度 */
|
||||
}
|
||||
|
||||
.toast-icon {
|
||||
font-size: 16px;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
font-size: 13px;
|
||||
max-width: calc(100% - 50px); /* 移动设备上调整最大宽度 */
|
||||
}
|
||||
|
||||
.toast-close {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.toast-close i {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,8 @@
|
||||
@import './components/date-range-picker.css';
|
||||
@import './components/upload-area.css';
|
||||
@import './components/file-tag.css';
|
||||
@import './components/message-modal.css';
|
||||
@import './components/toast.css';
|
||||
|
||||
/* @import './components/modal.css'; */
|
||||
|
||||
|
||||
@@ -94,6 +94,26 @@
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
max-width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* PDF文档样式约束 */
|
||||
.react-pdf__Document {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.react-pdf__Page {
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.react-pdf__Page__canvas {
|
||||
max-width: 100%;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.file-preview-actions {
|
||||
|
||||
+18
-1
@@ -1,3 +1,6 @@
|
||||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
dayjs.extend(utc);
|
||||
/**
|
||||
* 工具函数集合
|
||||
* 包含字段处理、防抖等通用功能
|
||||
@@ -65,4 +68,18 @@ export function getArrayDifference<T>(current: T[], previous: T[]): { added: T[]
|
||||
const removed = previous.filter(item => !current.includes(item));
|
||||
|
||||
return { added, removed };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 格式化日期时间
|
||||
export function formatDate(dateTime: string): string {
|
||||
if (!dateTime) return '';
|
||||
try {
|
||||
return dayjs.utc(dateTime).format('YYYY-MM-DD HH:mm:ss');
|
||||
} catch (error) {
|
||||
console.error('日期格式化失败:', error);
|
||||
return dateTime;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user