新增提示Toast组件

This commit is contained in:
2025-04-21 09:22:13 +08:00
parent 01d93522b8
commit 5c2c367856
36 changed files with 2609 additions and 478 deletions
+41 -8
View File
@@ -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;
+37 -2
View File
@@ -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 = () => {
+95 -44
View File
@@ -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>
+300 -114
View File
@@ -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>
{/* 评查点所属分组 */}
+66 -7
View File
@@ -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);
};
// 更新规则配置中的可用字段但保留已选择的字段和规则配置
+303
View File
@@ -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}
/>
)}
</>
);
}
+257
View File
@@ -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}
/>
))}
</>
);
}