优化评查结果的显示效果

This commit is contained in:
2025-06-01 18:30:39 +08:00
parent 7c935a8c8c
commit 529ed8072b
13 changed files with 2989 additions and 266 deletions
+17
View File
@@ -345,8 +345,25 @@ export async function getReviewPoints(fileId: string) {
// evaluatedPointResultsLog: {
// rules:[
// {
// "id": "0",
// "type": "consistency",
// "res": true,
// "config": {
// "logic": "all",
// "pairs": [
// {
// "sourceField": {"证据先行登记保存批准书-负责人意见并签名-时间": {page: 1,value: ''}},
// "targetField": {"证据先行登记保存批准书-负责人意见并签名-签名": {page: 2,value: '有无判断类型'}},
// "compareMethod": "exact",
// "res": true
// }
// ]
// }
// },
// {
// "id": "1",
// "type": "consistency",
// "res": false,
// "config": {
// "logic": "and",
// "pairs": [
+1 -16
View File
@@ -1,22 +1,7 @@
import { postgrestGet, postgrestPut, postgrestPost, type PostgrestParams } from '../postgrest-client';
import { postgrestGet, type PostgrestParams } from '../postgrest-client';
import dayjs from 'dayjs';
// import { API_BASE_URL } from '../client';
/**
* 格式化日期
* @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 响应中提取数据
-7
View File
@@ -20,13 +20,6 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) {
const [expandedMenus, setExpandedMenus] = useState<Record<string, boolean>>({});
const menuItems: MenuItem[] = [
{
id: 'contract-search',
title: '智能搜索',
path: '/contract-search',
hideBreadcrumb: true,
icon: 'ri-search-line'
},
{
id: 'contract-template',
title: '合同模板',
+419 -205
View File
@@ -17,9 +17,11 @@
* - 建议修改区域: 显示建议的修改内容
* - 操作按钮: 提供一键替换和人工审核功能
*/
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef } from 'react';
import { toastService } from '../ui/Toast';
// import { toastService } from '../ui/Toast';
import { createPortal } from 'react-dom'; // 导入React Portal API,用于将组件渲染到DOM树的不同位置
import { Tooltip } from '../ui/Tooltip';
// import '../../styles/components/TooltipStyles.css';
/**
* 比较方法映射
@@ -114,6 +116,7 @@ export interface ReviewPoint {
rules: Array<{
id: string;
type: string;
res?: boolean;
config: Record<string, unknown>;
}>;
};
@@ -143,6 +146,194 @@ interface ReviewPointsListProps {
onStatusChange: (id: string, editAuditStatusId: string | number, status: string, message: string) => void;
}
/**
* 全局状态对象,存储当前活动的提示框信息
* 这种方式避免了复杂的状态提升或Context API的使用
*/
let activeTooltip = {
show: false, // 控制提示框是否显示
content: null as React.ReactNode, // 提示框内容(React节点)
position: { top: 0, left: 0 } // 提示框在屏幕上的位置
};
/**
* 提示框Portal组件
*
* 使用React Portal将提示框渲染到document.body下,
* 这样可以确保提示框不受任何父元素overflow或z-index限制
*/
function TooltipPortal() {
// 使用本地状态保存提示框信息的副本
const [tooltip, setTooltip] = useState(activeTooltip);
useEffect(() => {
// 通过自定义事件机制监听全局tooltip状态更新
const updateTooltip = () => {
// 使用扩展运算符创建对象副本,确保状态更新被React检测到
setTooltip({...activeTooltip});
};
// 添加事件监听器
window.addEventListener('tooltip-update', updateTooltip);
// 组件卸载时清理事件监听器
return () => {
window.removeEventListener('tooltip-update', updateTooltip);
};
}, []);
// 如果不显示或没有内容,则不渲染任何东西
if (!tooltip.show || !tooltip.content) return null;
// 使用createPortal将提示框内容渲染到document.body
return createPortal(
<div
className="fixed bg-white shadow-lg rounded-md p-1 border border-gray-200 z-[9999]"
style={{
top: `${tooltip.position.top}px`,
left: `${tooltip.position.left}px`,
transform: 'translate(-100%, -50%)' // 调整位置,使提示框在指针左侧居中显示
}}
>
{tooltip.content}
{/* 添加小三角形指向提示框指向的元素 */}
<div className="absolute top-1/2 right-0 transform translate-x-1/2 -translate-y-1/2 rotate-45 w-2 h-2 bg-white border-t border-r border-gray-200"></div>
</div>,
document.body // 将内容挂载到body元素,完全脱离原组件DOM结构
);
}
/**
* 显示提示框的辅助函数
* @param content 要显示的React节点内容
* @param position 显示位置坐标
*/
function showTooltip(content: React.ReactNode, position: { top: number; left: number }): void {
// 更新全局状态对象
activeTooltip = {
show: true,
content,
position
};
// 触发自定义事件,通知TooltipPortal组件更新状态
window.dispatchEvent(new Event('tooltip-update'));
}
/**
* 隐藏提示框的辅助函数
*/
function hideTooltip(): void {
// 设置为不显示状态
activeTooltip.show = false;
// 触发自定义事件,通知TooltipPortal组件更新状态
window.dispatchEvent(new Event('tooltip-update'));
}
/**
* React组件表格Tooltip
* 将文本数据解析为表格并使用React组件渲染
* 条件性Tooltip组件
* 只有当内容超过2行时才显示tooltip
*/
const ReactTableTooltip = ({ content }: { content: string }) => {
const [showTooltip, setShowTooltip] = useState(false);
const textRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const checkTextOverflow = () => {
const element = textRef.current;
if (element) {
setShowTooltip(element.scrollHeight > element.clientHeight);
}
};
setTimeout(checkTextOverflow, 0);
window.addEventListener('resize', checkTextOverflow);
return () => {
window.removeEventListener('resize', checkTextOverflow);
};
}, [content]);
// 解析表格数据
const parseTableData = (text: string) => {
const rows = text.split('\n').map(row => row.split('\t'));
return rows;
};
// 渲染React表格
const renderReactTable = (text: string) => {
try {
const tableData = parseTableData(text);
const hasHeader = tableData.length > 0;
return (
<div className="overflow-auto max-h-[400px]">
<table className="min-w-full border-collapse border border-gray-300">
{hasHeader && (
<thead>
<tr>
{tableData[0].map((cell, cellIndex) => (
<th
key={`header-${cellIndex}`}
className="px-2 py-1 border border-gray-300 bg-gray-100 font-medium text-xs text-left"
>
{cell || ' '}
</th>
))}
</tr>
</thead>
)}
<tbody>
{tableData.slice(1).map((row, rowIndex) => (
<tr key={`row-${rowIndex}`}>
{row.map((cell, cellIndex) => (
<td
key={`cell-${rowIndex}-${cellIndex}`}
className="px-2 py-1 border border-gray-300 text-xs text-left"
>
{cell || ' '}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
} catch (error) {
console.error('表格渲染错误:', error);
return <div className="text-red-500"></div>;
}
};
// 检测内容是否像表格
const isTableLike = content.includes('\t') && content.includes('\n');
return (
<div className="text-xs p-1 rounded cursor-text w-full text-left">
{showTooltip ? (
<Tooltip
content={isTableLike ? renderReactTable(content) : content}
placement="top"
theme="light"
trigger="hover"
showArrow={true}
className="tooltip-custom-offset"
>
<div className="text-gray-800 break-all overflow-hidden line-clamp-2" ref={textRef}>
{content}
</div>
</Tooltip>
) : (
<div className="text-gray-800 break-all overflow-hidden line-clamp-2" ref={textRef}>
{content}
</div>
)}
</div>
);
};
export function ReviewPointsList({
reviewPoints,
statistics,
@@ -410,68 +601,74 @@ export function ReviewPointsList({
* 渲染评查点状态标签
* @param status 状态文本
* @param result 评查结果
* @param title 标签提示内容
* @returns 状态标签组件
*/
const renderStatusBadge = (status: string, result?: boolean) => {
const renderStatusBadge = (status: string, result?: boolean, title?: string) => {
// 优先根据result判断是否通过
if (result === true) {
return (
<span className="status-badge status-success">
<Tooltip
content={
<div className="p-1 gap-1 flex flex-col max-w-xs">
{title && <div className="text-xs text-gray-600">{title}</div>}
</div>
}
placement="top"
theme="light"
trigger="hover"
showArrow={true}
className="tooltip-custom-offset tooltip-top"
>
<span className="status-badge status-success text-xs m-1">
<i className="ri-checkbox-circle-line mr-1"></i>
</span>
</Tooltip>
);
}
// 当result为false时,根据status决定显示警告还是错误
if (result === false) {
if (status === 'warning') {
if (status === 'warning' || status === 'info') {
return (
<span className="status-badge status-warning">
<Tooltip
content={
<div className="p-1 flex flex-col gap-1 max-w-xs">
{title && <div className="text-xs text-gray-600">{title}</div>}
</div>
}
placement="top"
theme="light"
trigger="hover"
showArrow={true}
className="tooltip-custom-offset tooltip-top"
>
<span className="status-badge status-warning text-xs m-1">
<i className="ri-alert-line mr-1"></i>
</span>
</Tooltip>
);
} else if (status === 'error') {
return (
<span className="status-badge status-error">
<Tooltip
content={
<div className="p-1 flex flex-col gap-1 max-w-xs">
{title && <div className="text-xs text-gray-600">{title}</div>}
</div>
}
placement="top"
theme="light"
trigger="hover"
showArrow={true}
className="tooltip-custom-offset tooltip-top"
>
<span className="status-badge status-error text-xs m-1">
<i className="ri-close-circle-line mr-1"></i>
</span>
</Tooltip>
);
}
}
// 兼容旧版逻辑,当没有result时,仍按status判断
switch (status) {
case 'success':
return (
<span className="status-badge status-success">
<i className="ri-checkbox-circle-line mr-1"></i>
</span>
);
case 'warning':
return (
<span className="status-badge status-warning">
<i className="ri-alert-line mr-1"></i>
</span>
);
case 'error':
return (
<span className="status-badge status-error">
<i className="ri-close-circle-line mr-1"></i>
</span>
);
case 'processing':
return (
<span className="status-badge status-processing">
<i className="ri-time-line mr-1"></i>
</span>
);
default:
return (
<span className="status-badge status-warning">
<i className="ri-alert-line mr-1"></i>
</span>
);
}
};
/**
@@ -482,7 +679,7 @@ export function ReviewPointsList({
const renderHumanReviewBadge = (reviewPoint: ReviewPoint) => {
if (reviewPoint.postAction === 'manual') {
return (
<span className="status-badge status-waiting mt-1 text-xs">
<span className=" bg-[#f9f0ff] text-[#722ed1] text-xs rounded-sm p-0.5 my-auto">
<i className="ri-user-line mr-1"></i>
</span>
);
@@ -529,6 +726,7 @@ export function ReviewPointsList({
{/* 渲染各个一致性的规则分组 */}
{reviewPoint.evaluatedPointResultsLog?.rules?.map((rule, index) => {
// console.log('rule-------', rule);
// if (rule.type === 'consistency' && rule.res === true) {
if (rule.type === 'consistency') {
// console.log('rule-------', rule);
return <div key={`rule-${index}`}>
@@ -636,7 +834,8 @@ export function ReviewPointsList({
/**
* 渲染评查点一致性的规则的样式
* @param singleReviewPoint 评查点
* @param singleReviewPoint 一个评查点的一致性规则对象
* @param reviewPoint 评查点
* @returns 评查点一致性的规则的样式
*/
const renderConsistencyRule = (singleReviewPoint: Record<string, unknown>,reviewPoint: ReviewPoint) => {
@@ -907,7 +1106,7 @@ export function ReviewPointsList({
}
}
if (!hasPage) {
toastService.error('没有找到有效的页码');
// toastService.error('没有找到有效的页码');
}
}}
onKeyDown={(e) => {
@@ -964,12 +1163,16 @@ export function ReviewPointsList({
>
<div className="flex justify-between w-full">
{/* <span className="font-medium">{item.field}:</span> */}
<span className="w-full overflow-hidden line-clamp-2 hover:line-clamp-none hover:shadow-[0_0_10px_rgba(0,0,0,0.1)]
hover: z-10 hover:overflow-auto rounded transition-all duration-300 ease-in-out max-h-96">{item.data.value?.toString() || ''}
{/* <span className="w-full overflow-hidden line-clamp-2 hover:line-clamp-none
hover: z-10 hover:overflow-auto rounded max-h-96">{item.data.value?.toString() || ''}
{!item.data.page && !item.data.value && (
<i className="ri-information-line text-red-500 text-xs"></i>
)}
</span>
</span> */}
<ReactTableTooltip content={item.data.value?.toString() || ''} />
{!item.data.page && !item.data.value && (
<i className="ri-information-line text-red-500 text-xs" title="没有找到对应的文书内容"></i>
)}
</div>
</button>
))}
@@ -981,32 +1184,36 @@ export function ReviewPointsList({
) : (
<i className="ri-alert-line text-warning text-base" ></i>
)}
{/* 悬停提示框 - 横向布局 */}
<div className="w-auto absolute hidden group-hover:block right-full top-1/2 transform -translate-y-1/2 mr-2 z-100">
<div className="bg-white shadow-md rounded-md p-1 border border-gray-200">
{/* <div className="text-xs font-medium text-gray-700 w-auto">规则检查结果</div> */}
{/* 使用鼠标事件处理悬停提示 */}
<div
className="w-full h-full absolute top-0 left-0"
onMouseEnter={(e) => {
// 获取元素位置信息
const rect = e.currentTarget.getBoundingClientRect();
// 创建提示框内容
const content = (
<div className="flex flex-row gap-2 overflow-x-auto">
{chain.map((item, idx) =>
idx >= 1 ? (
<div key={idx} className={`rounded-md flex flex-row items-center
${
res ? 'bg-green-100 text-green-600': 'bg-yellow-100 text-yellow-600'
}`}>
<div key={idx} className={`rounded-md flex flex-row items-center`}>
<div className="text-xs text-gray-600 whitespace-nowrap pl-2">
{typeof item.compareMethod === 'object'
? ''
: getCompareMethodText(item.compareMethod)}
: `${getCompareMethodText(item.compareMethod)}:`}
</div>
<div className={`p-1 text-xs font-medium rounded-full min-w-[50px] text-center`}>
<div className={`p-1 text-xs rounded-full min-w-[50px] text-center`}>
{res ? '通过' : '不通过'}
</div>
</div>
) : null
)}
</div>
</div>
<div className="absolute top-1/2 right-0 transform translate-x-1/2 -translate-y-1/2 rotate-45 w-2 h-2 bg-white border-t border-r border-gray-200"></div>
</div>
);
// 显示提示框
showTooltip(content, { top: rect.top + rect.height/2, left: rect.left });
}}
onMouseLeave={hideTooltip}
/>
</div>
</div>
</div>
@@ -1014,7 +1221,6 @@ export function ReviewPointsList({
}
// 如果是标准的成对比较(2个元素)
return (
<div
key={`pair_${chainIndex}`}
@@ -1043,11 +1249,10 @@ export function ReviewPointsList({
>
<div className="value-source text-xs text-gray-500 mb-1">{chain[0].field}
{!chain[0].data.page && !chain[0].data.value && (
<i className="ri-information-line text-red-500 text-xs ml-1"></i>
<i className="ri-information-line text-red-500 text-xs ml-1" title="没有找到对应的文书内容"></i>
)}
</div>
<div className="value-content text-xs overflow-hidden line-clamp-2 hover:line-clamp-none hover:shadow-[0_0_10px_rgba(0,0,0,0.1)]
hover:z-10 hover:overflow-auto rounded transition-all duration-300 ease-in-out cursor-text max-h-96">{chain[0].data.value?.toString() || ''}</div>
<ReactTableTooltip content={chain[0].data.value?.toString() || ''} />
</button>
<button
className={`value-box flex flex-col flex-1 p-2 text-left ${res ? 'hover:bg-[rgba(0,128,0,0.1)]' : 'hover:bg-[rgba(255,255,0,0.1)]'} transition-colors hover:shadow-[0_0_10px_rgba(0,0,0,0.1)]`}
@@ -1066,11 +1271,10 @@ export function ReviewPointsList({
>
<div className="value-source text-xs text-gray-500 mb-1">{chain[1].field}
{!chain[1].data.page && !chain[1].data.value && (
<i className="ri-information-line text-red-500 text-xs ml-1"></i>
<i className="ri-information-line text-red-500 text-xs ml-1" title="没有找到对应的文书内容"></i>
)}
</div>
<div className="value-content text-xs overflow-hidden line-clamp-2 hover:line-clamp-none hover:shadow-[0_0_10px_rgba(0,0,0,0.1)]
hover:z-10 hover:overflow-auto rounded transition-all duration-300 ease-in-out cursor-text max-h-96">{chain[1].data.value?.toString() || ''}</div>
<ReactTableTooltip content={chain[1].data.value?.toString() || ''} />
</button>
</div>
<div className="status-indicator tooltip w-8 flex items-center justify-center group relative">
@@ -1079,28 +1283,35 @@ export function ReviewPointsList({
) : (
<i className="ri-alert-line text-warning text-base"></i>
)}
{/* 悬停提示框 - 横向布局 */}
<div className="w-auto absolute hidden group-hover:block right-full top-1/2 transform -translate-y-1/2 mr-2 z-100">
<div className="bg-white shadow-md rounded-md p-1 border border-gray-200">
{/* 使用鼠标事件处理悬停提示 */}
<div
className="w-full h-full absolute top-0 left-0"
onMouseEnter={(e) => {
// 获取元素位置信息
const rect = e.currentTarget.getBoundingClientRect();
// 创建提示框内容
const content = (
<div className="flex flex-row gap-2 overflow-x-auto">
<div key={chain[1].compareMethod} className={`rounded-md flex flex-row items-center ${
res
? 'bg-green-100 text-green-600'
: 'bg-yellow-100 text-yellow-600'
}`}>
<div key={chain[1].compareMethod} className={`rounded-md flex flex-row items-center`}>
<div className="text-xs text-gray-600 whitespace-nowrap pl-1">
{typeof chain[1].compareMethod === 'object'
? ''
: getCompareMethodText(chain[1].compareMethod)}
: `${getCompareMethodText(chain[1].compareMethod)}:`}
</div>
<div className={`p-1 text-xs font-medium rounded-full min-w-[50px] text-center`}>
<div className={`p-1 text-xs rounded-full min-w-[50px] text-center`}>
{res ? '通过' : '不通过'}
</div>
</div>
</div>
</div>
<div className="absolute top-1/2 right-0 transform translate-x-1/2 -translate-y-1/2 rotate-45 w-2 h-2 bg-white border-t border-r border-gray-200"></div>
</div>
);
// 显示提示框,稍微向下偏移,便于鼠标移动到tooltip上
showTooltip(content, {
top: rect.top + rect.height/2,
left: rect.left
});
}}
onMouseLeave={hideTooltip}
/>
</div>
</div>
);
@@ -1143,6 +1354,41 @@ export function ReviewPointsList({
const [, mainTypeValue] = mainTypeEntry;
/**
* 创建提示框内容
* 这个函数返回一个React节点,用于在提示框中显示
* 它将为每种规则类型(exists/format/logic/regex)创建一个带有状态标识的项目
*/
const createTooltipContent = () => {
return (
<div className="flex flex-row gap-2 overflow-x-auto max-h-[300px]">
{Object.entries(fieldValue?.type || {}).map(([typeKey, typeValue]) => (
<div key={typeKey} className={`rounded-md flex flex-row items-center`}>
<div className="text-xs text-gray-600 pl-1 whitespace-nowrap">{getRuleTypeText(typeKey)}:</div>
<div className={`p-1 text-xs rounded-full min-w-[50px] text-center`}>
{typeValue.res ? '通过' : '不通过'}
</div>
</div>
))}
</div>
);
};
/**
* 处理鼠标悬停事件
* 当鼠标悬停在状态指示器上时,计算提示框应该显示的位置并显示提示框
* @param e 鼠标事件对象
*/
const handleMouseEnter = (e: React.MouseEvent): void => {
// 获取触发元素的位置信息
const rect = e.currentTarget.getBoundingClientRect();
// 调用全局函数显示提示框,传递内容和位置信息
showTooltip(
createTooltipContent(),
{ top: rect.top + rect.height/2, left: rect.left }
);
};
return (
<button
className={`border border-gray rounded-md overflow-hidden mb-2 ${overallResult ? 'bg-[rgba(246,255,237,1)]' : 'bg-[rgba(255,251,230,1)]'} flex w-full text-left
@@ -1173,7 +1419,7 @@ export function ReviewPointsList({
<div className="text-xs text-gray-500 mb-1">
{fieldKey}
{!mainTypeValue.page && !mainTypeValue.value && (
<i className="ri-information-line text-red-500 text-xs ml-1"></i>
<i className="ri-information-line text-red-500 text-xs ml-1" title="没有找到对应的文书内容"></i>
)}
{/* 缺失显示 */}
{mainTypeValue.res === false && !mainTypeValue.value && (
@@ -1183,74 +1429,22 @@ export function ReviewPointsList({
)}
</div>
{/* 规则类型标签组 - 只在有多个类型时显示,且只显示非主要类型 */}
{/* {Object.keys(fieldValue?.type || {}).length > 1 && (
<div className="flex flex-wrap gap-1 mb-2">
{Object.entries(fieldValue?.type || {})
.filter(([typeKey]) => typeKey !== mainTypeKey) // 排除主要类型
.map(([typeKey, typeValue]) => (
<button
key={typeKey}
className={`text-xs px-2 py-0.5 rounded-full ${typeValue.res ? 'bg-green-100 text-green-800 hover:bg-green-300' : 'bg-yellow-100 text-yellow-800'}`}
onClick={() => {
if (typeValue.page && typeof onReviewPointSelect === 'function') {
onReviewPointSelect(reviewPoint.id, Number(typeValue.page));
}
}}
title={`${typeKey}: ${typeValue.res ? '通过' : '不通过'}`}
>
{typeKey}
</button>
))}
</div>
)} */}
{/* 主要值显示 */}
{mainTypeValue.value && (
<button
className="text-xs p-1 rounded cursor-text w-full text-left"
// onClick={(e) => {
// if (mainTypeValue.page && typeof onReviewPointSelect === 'function') {
// e.stopPropagation();
// onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page));
// }
// }}
>
<div className="text-gray-800 break-all overflow-hidden line-clamp-2 hover:line-clamp-none hover:shadow-[0_0_10px_rgba(0,0,0,0.1)]
hover: z-10 hover:overflow-auto rounded transition-all duration-300 ease-in-out cursor-text max-h-96">
{mainTypeValue.value}
</div>
</button>
<ReactTableTooltip content={mainTypeValue.value} />
)}
</div>
<div className={`w-8 flex items-center justify-center rounded-r-md group relative`}>
{/* 状态指示器 - 绑定鼠标事件用于显示/隐藏提示框 */}
<div
className="w-8 flex items-center justify-center rounded-r-md"
onMouseEnter={handleMouseEnter} // 鼠标进入时显示提示框
onMouseLeave={hideTooltip} // 鼠标离开时隐藏提示框
>
{overallResult ? (
<i className="ri-check-line text-success text-base hover:text-green-800" ></i>
) : (
<i className="ri-alert-line text-warning text-base hover:text-yellow-800" ></i>
)}
{/* 悬停提示框 - 横向布局 */}
<div className="w-auto absolute hidden group-hover:block right-full top-1/2 transform -translate-y-1/2 mr-2 z-100">
<div className="bg-white shadow-md rounded-md p-1 border border-gray-200">
{/* <div className="text-xs font-medium text-gray-700 w-auto">规则检查结果</div> */}
<div className="flex flex-row gap-2 overflow-x-auto">
{Object.entries(fieldValue?.type || {}).map(([typeKey, typeValue]) => (
<div key={typeKey} className={`rounded-md flex flex-row items-center ${
typeValue.res
? 'bg-green-100 text-green-600'
: 'bg-yellow-100 text-yellow-600'
}`}>
<div className="text-xs text-gray-600 pl-1 whitespace-nowrap">{getRuleTypeText(typeKey)}</div>
<div className={`p-1 text-xs font-medium rounded-full min-w-[50px] text-center`}>
{typeValue.res ? '通过' : '不通过'}
</div>
</div>
))}
</div>
</div>
<div className="absolute top-1/2 right-0 transform translate-x-1/2 -translate-y-1/2 rotate-45 w-2 h-2 bg-white border-t border-r border-gray-200"></div>
</div>
</div>
</button>
);
@@ -1260,12 +1454,19 @@ export function ReviewPointsList({
/**
* 渲染评查点大模型判断的规则的样式
* @param aiRule 评查点大模型判断的规则
* @param reviewPoint 关联的评查点
* @returns 评查点大模型判断的规则的样式
*
* 该函数处理AI模型评估的结果展示,包括:
* 1. 从规则配置中提取字段和评估结果
* 2. 为每个字段创建可点击的UI元素,显示内容和评估状态
* 3. 展示模型的评估消息
* 4. 处理字段点击导航到相应页面的逻辑
*
* @param aiRule 评查点大模型判断的规则对象
* @param reviewPoint 关联的评查点对象
* @returns React组件,用于显示AI模型评估结果
*/
const renderModelRule = (aiRule: Record<string, unknown>, reviewPoint: ReviewPoint) => {
// console.log('aiRule-------', aiRule);
// 从aiRule中提取配置信息
const config = aiRule.config as {
model?: string;
fields?: Record<string, {
@@ -1276,16 +1477,12 @@ export function ReviewPointsList({
res?: boolean;
} | undefined;
// 如果配置不存在,不渲染任何内容
if (!config) return null;
// 创建一个数组来存储需要渲染的JSX元素
const fieldElements: JSX.Element[] = [];
// 先存放一条粗的横线
// fieldElements.push(
// <div key="line" className=" bg-gray-50 rounded border border-gray-200 text-xs mb-3"></div>
// );
// 遍历fields,获取每个字段的值并生成对应的JSX元素
if (config.fields) {
Object.entries(config.fields).forEach(([key, value], index) => {
@@ -1305,6 +1502,7 @@ export function ReviewPointsList({
}
}}
onKeyDown={(e) => {
// 键盘导航支持
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
if (value.page && typeof onReviewPointSelect === 'function') {
@@ -1318,13 +1516,13 @@ export function ReviewPointsList({
type="button"
aria-label={`查看${key}内容详情`}
>
<div className="p-1 flex-1">
<div className="p-1 flex-1 text-left">
{/* 字段名称 */}
<div className="text-xs text-left text-gray-500 mb-1">
{key}
{/* 没有抽取到目录内容,page和value都为空 */}
{!value.page && !value.value && (
<i className="ri-information-line text-red-500 text-xs ml-1"></i>
<i className="ri-information-line text-red-500 text-xs ml-1" title="没有找到对应的文书内容"></i>
)}
{/* 缺失显示 */}
{!res && (
@@ -1336,19 +1534,7 @@ export function ReviewPointsList({
{/* 主要值显示 */}
{res && (
<button
className="text-xs p-1 rounded cursor-text w-full text-left"
// onClick={() => {
// if (value.page && typeof onReviewPointSelect === 'function') {
// onReviewPointSelect(reviewPoint.id, Number(value.page));
// }
// }}
>
<div className="text-gray-800 break-all overflow-hidden line-clamp-2 hover:line-clamp-none hover:shadow-[0_0_10px_rgba(0,0,0,0.1)]
hover: z-10 hover:overflow-auto rounded transition-all duration-300 ease-in-out cursor-text max-h-96">
{value.value}
</div>
</button>
<ReactTableTooltip content={value.value} />
)}
</div>
<div className={`w-8 flex items-center justify-center rounded-r-md group relative`}>
@@ -1357,38 +1543,45 @@ export function ReviewPointsList({
) : (
<i className="ri-alert-line text-warning text-base hover:text-yellow-800" ></i>
)}
{/* 悬停提示框 - 横向布局 */}
<div className="w-auto absolute hidden group-hover:block right-full top-1/2 transform -translate-y-1/2 mr-2 z-100">
<div className="bg-white shadow-md rounded-md p-1 border border-gray-200">
{/* <div className="text-xs font-medium text-gray-700 w-auto">规则检查结果</div> */}
{/* 使用鼠标事件处理悬停提示 */}
<div
className="w-full h-full absolute top-0 left-0"
onMouseEnter={(e) => {
// 获取元素位置信息
const rect = e.currentTarget.getBoundingClientRect();
// 创建提示框内容
const content = (
<div className="flex flex-row gap-2 overflow-x-auto">
<div className={`rounded-md flex flex-row items-center ${
res
? 'bg-green-100 text-green-600'
: 'bg-yellow-100 text-yellow-600'
}`}>
<div className="text-xs text-gray-600 pl-1 whitespace-nowrap"></div>
<div className={`p-1 text-xs font-medium rounded-full min-w-[50px] text-center`}>
<div className={`rounded-md flex flex-row items-center`}>
<div className="text-xs text-gray-600 pl-1 whitespace-nowrap">:</div>
<div className={`p-1 text-xs rounded-full min-w-[50px] text-center`}>
{res ? '通过' : '不通过'}
</div>
</div>
</div>
</div>
<div className="absolute top-1/2 right-0 transform translate-x-1/2 -translate-y-1/2 rotate-45 w-2 h-2 bg-white border-t border-r border-gray-200"></div>
</div>
);
// 显示提示框,稍微向下偏移,便于鼠标移动到tooltip上
showTooltip(content, {
top: rect.top + rect.height/2,
left: rect.left
});
}}
onMouseLeave={hideTooltip}
/>
</div>
</button>
);
});
}
// 后处理message
// 渲染AI模型返回的评估消息
if (config.message) {
// 检查message是否为对象,如果是则转换为字符串
const messageContent = typeof config.message === 'object'
? JSON.stringify(config.message)
: String(config.message);
// 添加模型评估消息区域,使用蓝色背景突出显示
fieldElements.push(
<div key="message" className="p-2 bg-blue-50 rounded border border-blue-200 text-xs mb-3 select-text">
<div className="flex flex-row items-center">
@@ -1406,12 +1599,24 @@ export function ReviewPointsList({
/**
* 过滤评查点中的规则,把type是exist、format、logic、regex的规则中重复的进行去重
* @param reviewPoint 评查点
* @returns 合并后的规则数组
* 过滤评查点中的规则,把type是exists、format、logic、regex的规则中重复的进行去重和合并
*
* 该函数的主要作用:
* 1. 从评查点的evaluatedPointResultsLog中提取特定类型的规则
* 2. 将相同字段(fieldKey)的不同规则类型结果合并到一起
* 3. 为UI渲染准备统一结构的数据
*
* 支持的规则类型:
* - exists: 有无判断规则
* - format: 格式判断规则
* - logic: 逻辑判断规则
* - regex: 正则表达式规则
*
* @param reviewPoint 评查点对象
* @returns 合并后的规则数组,每个元素包含字段名和各类规则的评估结果
*/
const filterOtherRule = (reviewPoint: ReviewPoint) => {
// 遍历这些评查点中的规则evaluatedPointResultsLog,把type是exist、format、logic、regex的规则添加到allRule中
// 定义接口描述规则字段值的结构
interface RuleFieldValue {
page?: number | string;
value?: string;
@@ -1424,6 +1629,7 @@ export function ReviewPointsList({
}> = [];
for (const rule of reviewPoint.evaluatedPointResultsLog?.rules || []) {
// 处理"有无判断"类型的规则
if (rule.type === 'exists') {
// 使用类型断言获取config对象的具体结构
const config = rule.config as {
@@ -1469,6 +1675,8 @@ export function ReviewPointsList({
});
}
}
// 处理"格式判断"类型的规则
if (rule.type === 'format') {
// 使用类型断言获取config对象的具体结构
const config = rule.config as {
@@ -1491,7 +1699,7 @@ export function ReviewPointsList({
fieldKey: key,
fieldValue: {
...fieldValue,
type: { format: config.res }
type: { format: config.res } // 标记为format类型,结果为config.res
}
};
@@ -1499,6 +1707,8 @@ export function ReviewPointsList({
}
}
}
// 处理"逻辑判断"类型的规则
if (rule.type === 'logic') {
// 使用类型断言获取config对象的具体结构
const config = rule.config as {
@@ -1533,6 +1743,8 @@ export function ReviewPointsList({
});
}
}
// 处理"正则表达式"类型的规则
if (rule.type === 'regex') {
// 使用类型断言获取config对象的具体结构
const config = rule.config as {
@@ -1576,7 +1788,7 @@ export function ReviewPointsList({
};
}> = [];
// 使用对象存储相同fieldKey的项
// 使用对象存储相同fieldKey的项,便于快速查找和合并
const fieldKeyMap: Record<string, {
fieldKey: string;
fieldValue: {
@@ -1588,7 +1800,7 @@ export function ReviewPointsList({
};
}> = {};
// 第一步:按fieldKey分组
// 第一步:按fieldKey分组并合并不同类型的规则结果
allRule.forEach(item => {
const fieldKey = item.fieldKey;
const fieldValue = item.fieldValue;
@@ -1599,7 +1811,7 @@ export function ReviewPointsList({
const page = fieldValue.page;
const value = fieldValue.value;
// 如果是第一次遇到这个fieldKey
// 如果是第一次遇到这个fieldKey,创建新条目
if (!fieldKeyMap[fieldKey]) {
// 创建新的结构
fieldKeyMap[fieldKey] = {
@@ -1610,7 +1822,7 @@ export function ReviewPointsList({
};
}
// 将类型信息添加到type对象中
// 将类型信息添加到type对象中,允许一个字段有多种规则类型的结果
fieldKeyMap[fieldKey].fieldValue.type[typeKey] = {
res: typeValue,
page,
@@ -1623,8 +1835,7 @@ export function ReviewPointsList({
mergedRules.push(fieldKeyMap[key]);
}
// console.log('合并后的规则:', mergedRules);
// setOtherRule(mergedRules);
// 返回合并后的规则数组
return mergedRules;
};
@@ -1670,6 +1881,7 @@ export function ReviewPointsList({
{checkContentPage(reviewPoint).pageIndex === 0 && (
<p className="text-xs text-red-500 select-text text-left mb-1"></p>
)}
<div className="mt-2">
{/* {reviewPoint.suggestion && (
<div className="p-2 bg-blue-50 rounded border border-blue-200 text-xs mb-3 select-text">
@@ -1682,7 +1894,7 @@ export function ReviewPointsList({
{/* 评查点内容显示区域 */}
{reviewPoint.content && Object.entries(reviewPoint.content).length > 0 && (
<div className="p-2 bg-white rounded border border-gray-200 text-xs mb-3 select-text">
<div className="bg-white rounded border-gray-200 text-xs mb-3 select-text">
{/* 修改评查结果的结构之后,显示新的结构 */}
{renderContent(reviewPoint, mergedRules)}
</div>
@@ -1805,7 +2017,7 @@ export function ReviewPointsList({
{reviewPoint.content !== null && Object.keys(reviewPoint.content).length > 0 && (
<>
{/* 内容显示区域 */}
<div className="p-2 bg-white rounded border border-gray-200 text-xs mb-3 select-text">
<div className="bg-white rounded border-gray-200 text-xs mb-3 select-text">
<div>
{/* 修改评查结果的结构之后,显示新的结构 */}
{renderContent(reviewPoint, mergedRules)}
@@ -1969,6 +2181,7 @@ export function ReviewPointsList({
// 组件主渲染函数
return (
<div className="review-points-panel select-text">
<TooltipPortal />
{/* 面板头部 */}
<div className="review-panel-header py-2 px-4 flex items-center">
<i className="ri-file-list-check-line text-primary mr-2"></i>
@@ -1987,14 +2200,15 @@ export function ReviewPointsList({
filteredReviewPoints.map(reviewPoint => (
<div
key={reviewPoint.id}
className={`rounded-md review-point-item ${activeReviewPointResultId === reviewPoint.id ? 'active border-l-4 border-l-[#00684a] shadow-md' : 'border-l-4 border-l-transparent'}
transition-all duration-300 ease-in-out
hover:shadow-lg hover:-translate-x-0.5 hover:bg-[rgba(0,0,0,0.02)] my-2`}
className={`rounded-md review-point-item ${activeReviewPointResultId === reviewPoint.id ? 'active border-l-4 !border-l-[rgba(0,104,74,1)] shadow-md' : 'border-l-4 border-l-transparent'}
transition-all duration-300 ease-in-out shadow-sm
hover:shadow-lg hover:border-l-4 hover:border-l-[rgba(0,104,74,0.3)]
hover:bg-[rgba(0,0,0,0.02)] my-2`}
role="button"
tabIndex={0}
style={{ userSelect: 'text' }}
onClick={() => {
// console.log('reviewPoint', reviewPoint);
console.log('reviewPoint', reviewPoint);
handleReviewPointClick(reviewPoint.id);
}}
onKeyDown={(e) => {
@@ -2006,12 +2220,12 @@ export function ReviewPointsList({
{/* 评查点标题和状态 */}
{/* 评查点名称 pointName*/}
<div className="flex justify-between items-center mb-2">
<div className='flex flex-col'>
<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='flex flex-col'> */}
<div className="review-point-title text-left text-blue-500 max-w-[75%] break-all">{reviewPoint.pointName}</div>
{/* <div className="review-point-header flex justify-between items-start">
<div className="flex-1 text-left min-w-[25%] font-medium text-[13px]">{reviewPoint.title}</div>
{/* 评查点所属分组 */}
{/* <div className="review-point-location max-w-[40%] flex items-center">
//评查点分组显示
<div className="review-point-location max-w-[40%] flex items-center">
<i className="ri-file-list-line mr-1 flex-shrink-0"></i>
<span
className="truncate block whitespace-nowrap overflow-hidden
@@ -2021,11 +2235,11 @@ export function ReviewPointsList({
>
{reviewPoint.groupName}
</span>
</div>
</div> */}
</div>
</div>
<div className="flex flex-col items-center ml-2 flex-shrink-0 max-w-[15%]">
{renderStatusBadge(reviewPoint.status, reviewPoint.result)}
{/* </div> */}
<div className="flex ml-2 flex-shrink-0 min-w-[15%]">
{renderStatusBadge(reviewPoint.status, reviewPoint.result,reviewPoint.title)}
{renderHumanReviewBadge(reviewPoint)}
</div>
</div>
+3 -1
View File
@@ -105,7 +105,9 @@ export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfi
>
<i className="ri-lightbulb-line"></i> AI智能分析
</button> */}
{fileInfo.type === '1' && (
{/* {fileInfo.type === '1' && ( */}
{/* 隐藏结构比对 */}
{fileInfo.type === '999999' && (
<button
className={`tab-nav-item ${activeTab === 'filecompare' ? 'active' : ''}`}
onClick={() => onTabChange('filecompare')}
+295
View File
@@ -0,0 +1,295 @@
import { useState, useEffect, useRef, ReactNode } from 'react';
import { createPortal } from 'react-dom';
import '../../styles/components/TooltipStyles.css';
// 提示框主题
export type TooltipTheme = 'dark' | 'light' | 'primary' | 'success' | 'warning' | 'error' | 'info';
// 提示框位置
export type TooltipPlacement = 'top' | 'bottom' | 'left' | 'right';
// 提示框属性
export interface TooltipProps {
children: ReactNode; // 触发元素
content: ReactNode; // 提示内容
theme?: TooltipTheme; // 主题样式
placement?: TooltipPlacement; // 显示位置
rich?: boolean; // 是否为富文本提示
header?: ReactNode; // 富文本提示标题
footer?: ReactNode; // 富文本提示页脚
trigger?: 'hover' | 'click'; // 触发方式
visible?: boolean; // 是否显示(受控模式)
showArrow?: boolean; // 是否显示箭头
maxWidth?: number | string; // 最大宽度
className?: string; // 自定义类名
onVisibleChange?: (visible: boolean) => void; // 显示状态变化回调
}
/**
* Tooltip 组件
*
* 提供增强的悬浮提示功能,支持多种主题、位置和富文本模式
*/
export function Tooltip({
children,
content,
theme = 'dark',
placement = 'top',
rich = false,
header,
footer,
trigger = 'hover',
visible: controlledVisible,
showArrow = true,
maxWidth = 320,
className = '',
onVisibleChange
}: TooltipProps) {
// 使用内部状态管理提示框显示状态(非受控模式)
const [visible, setVisible] = useState(false);
// 判断是否为受控组件
const isControlled = controlledVisible !== undefined;
// 获取实际显示状态
const isVisible = isControlled ? controlledVisible : visible;
// 引用DOM元素
const triggerRef = useRef<HTMLDivElement>(null);
const tooltipRef = useRef<HTMLDivElement>(null);
// 处理显示状态变化
const handleVisibleChange = (newVisible: boolean) => {
if (!isControlled) {
setVisible(newVisible);
}
onVisibleChange?.(newVisible);
};
// 触发器事件处理
const triggerEvents = trigger === 'hover'
? {
onMouseEnter: () => handleVisibleChange(true),
onMouseLeave: () => handleVisibleChange(false),
}
: {
onClick: () => handleVisibleChange(!isVisible),
};
// 更新提示框位置的函数
const updateTooltipPosition = () => {
if (!isVisible || !triggerRef.current || !tooltipRef.current) return;
// 获取触发元素位置
const triggerRect = triggerRef.current.getBoundingClientRect();
const tooltipRect = tooltipRef.current.getBoundingClientRect();
// 设置最大宽度
tooltipRef.current.style.maxWidth = typeof maxWidth === 'number'
? `${maxWidth}px`
: String(maxWidth);
// 计算各个方向的位置
let top = 0, left = 0;
let arrowTop = 0, arrowLeft = 0;
const arrowSize = 8; // 箭头大小
const gap = 10; // 提示框与触发元素的间距
switch (placement) {
case 'top':
left = triggerRect.left + (triggerRect.width / 2) - (tooltipRect.width / 2);
top = triggerRect.top - tooltipRect.height - arrowSize;
arrowLeft = tooltipRect.width / 2 - arrowSize;
arrowTop = tooltipRect.height;
break;
case 'bottom':
left = triggerRect.left + (triggerRect.width / 2) - (tooltipRect.width / 2);
top = triggerRect.bottom + arrowSize;
arrowLeft = tooltipRect.width / 2 - arrowSize;
arrowTop = -arrowSize;
break;
case 'left':
left = triggerRect.left - tooltipRect.width - arrowSize;
top = triggerRect.top + (triggerRect.height / 2) - (tooltipRect.height / 2);
arrowLeft = tooltipRect.width;
arrowTop = tooltipRect.height / 2 - arrowSize;
break;
case 'right':
left = triggerRect.right + arrowSize;
top = triggerRect.top + (triggerRect.height / 2) - (tooltipRect.height / 2);
arrowLeft = -arrowSize;
arrowTop = tooltipRect.height / 2 - arrowSize;
break;
}
// 确保提示框不超出视口
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// 调整位置,避免超出视口
if (left < 0) {
left = gap;
arrowLeft = Math.max(triggerRect.left + (triggerRect.width / 2) - left - arrowSize, arrowSize * 2);
} else if (left + tooltipRect.width > viewportWidth) {
left = viewportWidth - tooltipRect.width - gap;
arrowLeft = Math.min(triggerRect.left + (triggerRect.width / 2) - left - arrowSize, tooltipRect.width - arrowSize * 2);
}
if (top < 0) {
if (placement === 'top') {
// 如果上方放不下,切换到下方
top = triggerRect.bottom + arrowSize;
arrowTop = -arrowSize;
tooltipRef.current.classList.remove('tooltip-top');
tooltipRef.current.classList.add('tooltip-bottom');
} else {
top = gap;
arrowTop = Math.max(triggerRect.top + (triggerRect.height / 2) - top - arrowSize, arrowSize * 2);
}
} else if (top + tooltipRect.height > viewportHeight) {
if (placement === 'bottom') {
// 如果下方放不下,切换到上方
top = triggerRect.top - tooltipRect.height - arrowSize;
arrowTop = tooltipRect.height;
tooltipRef.current.classList.remove('tooltip-bottom');
tooltipRef.current.classList.add('tooltip-top');
} else {
top = viewportHeight - tooltipRect.height - gap;
arrowTop = Math.min(triggerRect.top + (triggerRect.height / 2) - top - arrowSize, tooltipRect.height - arrowSize * 2);
}
}
// 应用位置样式
tooltipRef.current.style.position = 'fixed';
tooltipRef.current.style.top = `${top}px`;
tooltipRef.current.style.left = `${left}px`;
tooltipRef.current.style.transform = 'none'; // 重置任何变换
// 定位箭头
const arrow = tooltipRef.current.querySelector('.tooltip-arrow') as HTMLElement;
if (arrow && showArrow) {
switch (placement) {
case 'top':
arrow.style.bottom = `-${arrowSize}px`;
arrow.style.left = `${arrowLeft}px`;
arrow.style.top = 'auto';
arrow.style.right = 'auto';
break;
case 'bottom':
arrow.style.top = `-${arrowSize}px`;
arrow.style.left = `${arrowLeft}px`;
arrow.style.bottom = 'auto';
arrow.style.right = 'auto';
break;
case 'left':
arrow.style.right = `-${arrowSize}px`;
arrow.style.top = `${arrowTop}px`;
arrow.style.bottom = 'auto';
arrow.style.left = 'auto';
break;
case 'right':
arrow.style.left = `-${arrowSize}px`;
arrow.style.top = `${arrowTop}px`;
arrow.style.bottom = 'auto';
arrow.style.right = 'auto';
break;
}
}
};
// 计算提示框位置
useEffect(() => {
if (!isVisible) return;
// 初始化位置
updateTooltipPosition();
// 添加滚动和调整大小事件监听器
window.addEventListener('scroll', updateTooltipPosition, true);
window.addEventListener('resize', updateTooltipPosition);
// 定期更新位置(处理内容动态变化的情况)
const intervalId = setInterval(updateTooltipPosition, 300);
// 清理函数
return () => {
window.removeEventListener('scroll', updateTooltipPosition, true);
window.removeEventListener('resize', updateTooltipPosition);
clearInterval(intervalId);
};
}, [isVisible, placement, maxWidth, showArrow]);
// 处理点击外部关闭
useEffect(() => {
if (trigger !== 'click' || !isVisible) return;
const handleClickOutside = (event: MouseEvent) => {
if (
triggerRef.current &&
!triggerRef.current.contains(event.target as Node) &&
tooltipRef.current &&
!tooltipRef.current.contains(event.target as Node)
) {
handleVisibleChange(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [trigger, isVisible]);
// 渲染提示框内容
const renderTooltipContent = () => {
if (rich) {
return (
<div className="tooltip-content">
{header && <div className="tooltip-header">{header}</div>}
<div className="tooltip-body">{content}</div>
{footer && <div className="tooltip-footer">{footer}</div>}
</div>
);
}
return (
<div className="tooltip-content">
<div>{content}</div>
</div>
);
};
// 生成提示框类名
const tooltipClassNames = [
'tooltip-container',
`tooltip-${placement}`,
`tooltip-${theme}`,
rich ? 'tooltip-rich' : '',
isVisible ? 'tooltip-visible' : '',
className
].filter(Boolean).join(' ');
// 使用Portal渲染提示框
const tooltipPortal = isVisible && typeof document !== 'undefined'
? createPortal(
<div
ref={tooltipRef}
className={tooltipClassNames}
style={{ maxWidth }}
>
{renderTooltipContent()}
{showArrow && <div className="tooltip-arrow"></div>}
</div>,
document.body
)
: null;
return (
<div className="tooltip-trigger" ref={triggerRef} {...triggerEvents}>
{children}
{tooltipPortal}
</div>
);
}
export default Tooltip;
+35
View File
@@ -0,0 +1,35 @@
// 导出所有UI组件,方便统一引入
// 核心组件
export { Button } from './Button';
export { Card } from './Card';
export { Modal } from './Modal';
export { toastService } from './Toast';
export { Tooltip } from './Tooltip';
// 数据展示组件
export { Table } from './Table';
export { Pagination } from './Pagination';
export { Tag } from './Tag';
export { FileTag } from './FileTag';
export { FileTypeTag } from './FileTypeTag';
export { StatusBadge } from './StatusBadge';
export { StatusDot } from './StatusDot';
export { FileIcon } from './FileIcon';
// 数据输入组件
export { SearchBox } from './SearchBox';
export { DateRangePicker } from './DateRangePicker';
export { FilterPanel } from './FilterPanel';
export { UploadArea } from './UploadArea';
// 反馈组件
export { Alert } from './Alert';
export { MessageModal } from './MessageModal';
export { LoadingBar } from './LoadingBar';
export { RouteChangeLoader } from './RouteChangeLoader';
export { FileProgress } from './FileProgress';
export { ProcessingSteps } from './ProcessingSteps';
// 示例组件(开发环境使用)
export { TooltipExample } from '../../routes/examples/TooltipExample';
+236
View File
@@ -0,0 +1,236 @@
import { Tooltip } from '../../components/ui/Tooltip';
/**
* Tooltip 组件示例
* 展示不同主题、位置和风格的提示框
*/
export function TooltipExample() {
return (
<div className="tooltip-examples p-4">
<h2 className="text-lg font-bold mb-4">Tooltip </h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* 基础提示框 */}
<div className="example-section">
<h3 className="text-base font-semibold mb-2"></h3>
<div className="flex space-x-4 mb-4">
<Tooltip content="这是一个基础提示框">
<button className="px-3 py-1 bg-gray-200 rounded hover:bg-gray-300">
</button>
</Tooltip>
<Tooltip content="点击显示的提示框" trigger="click">
<button className="px-3 py-1 bg-gray-200 rounded hover:bg-gray-300">
</button>
</Tooltip>
</div>
</div>
{/* 不同位置 */}
<div className="example-section">
<h3 className="text-base font-semibold mb-2"></h3>
<div className="flex space-x-4 mb-4">
<Tooltip content="顶部提示" placement="top">
<button className="px-3 py-1 bg-gray-200 rounded hover:bg-gray-300">
</button>
</Tooltip>
<Tooltip content="底部提示" placement="bottom">
<button className="px-3 py-1 bg-gray-200 rounded hover:bg-gray-300">
</button>
</Tooltip>
<Tooltip content="左侧提示" placement="left">
<button className="px-3 py-1 bg-gray-200 rounded hover:bg-gray-300">
</button>
</Tooltip>
<Tooltip content="右侧提示" placement="right">
<button className="px-3 py-1 bg-gray-200 rounded hover:bg-gray-300">
</button>
</Tooltip>
</div>
</div>
{/* 不同主题 */}
<div className="example-section">
<h3 className="text-base font-semibold mb-2"></h3>
<div className="flex flex-wrap gap-2">
<Tooltip content="深色主题" theme="dark">
<span className="px-3 py-1 border border-gray-300 rounded cursor-default">
</span>
</Tooltip>
<Tooltip content="浅色主题" theme="light">
<span className="px-3 py-1 border border-gray-300 rounded cursor-default">
</span>
</Tooltip>
<Tooltip content="主题色" theme="primary">
<span className="px-3 py-1 border border-gray-300 rounded cursor-default">
</span>
</Tooltip>
<Tooltip content="成功提示" theme="success">
<span className="px-3 py-1 border border-gray-300 rounded cursor-default">
</span>
</Tooltip>
<Tooltip content="警告提示" theme="warning">
<span className="px-3 py-1 border border-gray-300 rounded cursor-default">
</span>
</Tooltip>
<Tooltip content="错误提示" theme="error">
<span className="px-3 py-1 border border-gray-300 rounded cursor-default">
</span>
</Tooltip>
<Tooltip content="信息提示" theme="info">
<span className="px-3 py-1 border border-gray-300 rounded cursor-default">
</span>
</Tooltip>
</div>
</div>
{/* 富文本提示框 */}
<div className="example-section">
<h3 className="text-base font-semibold mb-2"></h3>
<div className="flex space-x-4">
<Tooltip
content={
<div>
<div className="flex items-center mb-1">
<span className="w-24 text-gray-500">CPU使用率:</span>
<span className="text-green-500 font-medium">32%</span>
</div>
<div className="flex items-center mb-1">
<span className="w-24 text-gray-500">使:</span>
<span className="text-yellow-500 font-medium">76%</span>
</div>
<div className="flex items-center">
<span className="w-24 text-gray-500">:</span>
<span className="text-blue-500 font-medium">245GB/500GB</span>
</div>
</div>
}
theme="light"
rich={true}
header="系统性能报告"
footer="更新时间: 2023-10-15 15:30:42"
showArrow={true}
>
<button className="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600">
</button>
</Tooltip>
<Tooltip
content={
<div>
<div className="flex items-center justify-between mb-1">
<span>:</span>
<span className="text-green-400 font-medium">¥258,432</span>
</div>
<div className="flex items-center justify-between mb-1">
<span>:</span>
<span className="text-green-400 font-medium">+15.8%</span>
</div>
<div className="flex items-center justify-between">
<span>:</span>
<span className="text-yellow-400 font-medium"></span>
</div>
</div>
}
theme="dark"
rich={true}
header="销售数据分析"
footer="数据来源: 销售管理系统"
placement="bottom"
>
<button className="px-3 py-1 bg-green-500 text-white rounded hover:bg-green-600">
</button>
</Tooltip>
</div>
</div>
{/* 条件渲染示例 */}
<div className="example-section">
<h3 className="text-base font-semibold mb-2"></h3>
<div className="flex space-x-4">
<Tooltip
content="优秀:得分在90分以上"
theme="success"
>
<span className="text-green-500 font-bold px-2">95</span>
</Tooltip>
<Tooltip
content="良好:得分在60-90分之间"
theme="warning"
>
<span className="text-yellow-500 px-2">75</span>
</Tooltip>
<Tooltip
content="不及格:得分低于60分"
theme="error"
>
<span className="text-red-500 px-2">45</span>
</Tooltip>
</div>
</div>
{/* 自定义样式示例 */}
<div className="example-section">
<h3 className="text-base font-semibold mb-2"></h3>
<div className="flex space-x-4">
<Tooltip
content="带有自定义宽度的提示框"
maxWidth={150}
>
<span className="border-b border-dotted border-gray-500 cursor-help">
</span>
</Tooltip>
<Tooltip
content="没有箭头的提示框"
showArrow={false}
theme="primary"
>
<span className="border-b border-dotted border-blue-500 cursor-help">
</span>
</Tooltip>
<Tooltip
content="自定义类名"
className="custom-tooltip"
>
<span className="border-b border-dotted border-purple-500 cursor-help text-purple-500">
</span>
</Tooltip>
</div>
</div>
</div>
</div>
);
}
export default TooltipExample;
+2 -1
View File
@@ -548,7 +548,8 @@ export default function ReviewDetails() {
)}
{reviewData.fileInfo.uploadTime && (
<div className="text-xs text-gray-500">
| {reviewData.fileInfo.uploadTime} | {reviewData.fileInfo.uploadUser}
| {reviewData.fileInfo.uploadTime}
{/* | 上传用户:{reviewData.fileInfo.uploadUser} */}
</div>
)}
</div>
+249
View File
@@ -0,0 +1,249 @@
/* Tooltip 基础样式 */
.tooltip-trigger {
position: relative;
display: inline-block;
}
.tooltip-container {
position: absolute;
visibility: hidden;
opacity: 0;
z-index: 9999;
width: max-content;
max-width: 320px;
transition: opacity 0.3s ease, visibility 0.3s ease;
pointer-events: none;
}
.tooltip-visible {
visibility: visible;
opacity: 1;
pointer-events: auto;
}
.tooltip-content {
position: relative;
padding: 0.75rem 1rem;
border-radius: 0.375rem;
font-size: 0.875rem;
line-height: 1.5;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.tooltip-arrow {
position: absolute;
width: 0;
height: 0;
border-style: solid;
z-index: 1;
}
/* Tooltip 主题 */
.tooltip-dark .tooltip-content {
background-color: #1e293b;
color: #f1f5f9;
}
.tooltip-dark .tooltip-arrow {
border-color: #1e293b;
}
.tooltip-light .tooltip-content {
background-color: #ffffff;
color: #334155;
border: 1px solid #e2e8f0;
}
.tooltip-light .tooltip-arrow {
border-color: #ffffff;
}
.tooltip-primary .tooltip-content {
background-color: #2563eb;
color: #ffffff;
}
.tooltip-primary .tooltip-arrow {
border-color: #2563eb;
}
.tooltip-success .tooltip-content {
background-color: #22c55e;
color: #ffffff;
}
.tooltip-success .tooltip-arrow {
border-color: #22c55e;
}
.tooltip-warning .tooltip-content {
background-color: #f59e0b;
color: #ffffff;
}
.tooltip-warning .tooltip-arrow {
border-color: #f59e0b;
}
.tooltip-error .tooltip-content {
background-color: #ef4444;
color: #ffffff;
}
.tooltip-error .tooltip-arrow {
border-color: #ef4444;
}
.tooltip-info .tooltip-content {
background-color: #3b82f6;
color: #ffffff;
}
.tooltip-info .tooltip-arrow {
border-color: #3b82f6;
}
/* Tooltip 位置基本样式 */
.tooltip-top .tooltip-arrow {
border-width: 8px 8px 0 8px;
border-color: inherit transparent transparent transparent;
}
.tooltip-bottom .tooltip-arrow {
border-width: 0 8px 8px 8px;
border-color: transparent transparent inherit transparent;
}
.tooltip-left .tooltip-arrow {
border-width: 8px 0 8px 8px;
border-color: transparent transparent transparent inherit;
}
.tooltip-right .tooltip-arrow {
border-width: 8px 8px 8px 0;
border-color: transparent inherit transparent transparent;
}
/* 富文本提示框样式 */
.tooltip-rich .tooltip-content {
padding: 0;
overflow: hidden;
}
.tooltip-header {
padding: 0.5rem 1rem;
font-weight: 600;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.tooltip-body {
padding: 0.75rem 1rem;
}
.tooltip-footer {
padding: 0.5rem 1rem;
background-color: rgba(0, 0, 0, 0.05);
border-top: 1px solid rgba(255, 255, 255, 0.1);
font-size: 0.75rem;
text-align: right;
}
/* 深色主题特定样式调整 */
.tooltip-dark .tooltip-header {
border-bottom-color: rgba(255, 255, 255, 0.1);
}
.tooltip-dark .tooltip-footer {
background-color: rgba(255, 255, 255, 0.05);
border-top-color: rgba(255, 255, 255, 0.1);
}
/* 浅色主题特定样式调整 */
.tooltip-light .tooltip-header {
border-bottom-color: #e2e8f0;
}
.tooltip-light .tooltip-footer {
background-color: #f8fafc;
border-top-color: #e2e8f0;
}
/* 浅色主题下的箭头样式修正 */
.tooltip-light.tooltip-top .tooltip-arrow {
border-color: #ffffff transparent transparent transparent;
}
.tooltip-light.tooltip-bottom .tooltip-arrow {
border-color: transparent transparent #ffffff transparent;
}
.tooltip-light.tooltip-left .tooltip-arrow {
border-color: transparent transparent transparent #ffffff;
}
.tooltip-light.tooltip-right .tooltip-arrow {
border-color: transparent #ffffff transparent transparent;
}
/* 表格样式 */
.tooltip-content table {
border-collapse: collapse;
width: 100%;
font-size: 0.75rem;
}
.tooltip-content table th,
.tooltip-content table td {
padding: 0.25rem 0.5rem;
text-align: left;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
.tooltip-content table th {
font-weight: 600;
background-color: rgba(0, 0, 0, 0.05);
}
.tooltip-content table tr:nth-child(even) {
background-color: rgba(0, 0, 0, 0.02);
}
/* 深色主题下的表格样式 */
.tooltip-dark .tooltip-content table th,
.tooltip-dark .tooltip-content table td {
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.tooltip-dark .tooltip-content table th {
background-color: rgba(255, 255, 255, 0.1);
}
.tooltip-dark .tooltip-content table tr:nth-child(even) {
background-color: rgba(255, 255, 255, 0.05);
}
/* 动画效果 */
@keyframes tooltip-fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.tooltip-visible {
animation: tooltip-fade-in 0.2s ease-out;
}
/* 自定义偏移类 */
.tooltip-custom-offset.tooltip-top {
margin-top: 8px; /* 增加顶部边距 */
pointer-events: auto; /* 允许鼠标事件 */
}
/* 延长鼠标移入tooltip的路径 */
.tooltip-custom-offset .tooltip-arrow {
height: 12px; /* 增加箭头高度 */
pointer-events: auto; /* 允许鼠标事件 */
}
+3 -2
View File
@@ -26,6 +26,7 @@
@import './components/file-tag.css';
@import './components/message-modal.css';
@import './components/toast.css';
@import './components/TooltipStyles.css';
/* @import './components/modal.css'; */
@@ -132,7 +133,7 @@
/* === 侧边栏 === */
.sidebar {
@apply w-[280px] h-screen bg-white border-r border-gray-100 flex flex-col
@apply w-[240px] h-screen bg-white border-r border-gray-100 flex flex-col
transition-all duration-300 fixed left-0 top-0 z-[100] overflow-y-auto
shadow-[0_0_15px_rgba(0,0,0,0.05)];
}
@@ -199,7 +200,7 @@
/* === 主内容区域 === */
.main-content {
@apply flex-1 ml-[280px] transition-all duration-300 min-h-screen flex flex-col;
@apply flex-1 ml-[240px] transition-all duration-300 min-h-screen flex flex-col;
}
.main-content.sidebar-collapsed {
+5 -3
View File
@@ -225,7 +225,7 @@
.review-point-item:hover {
/* background-color: #f5f5f5; */
/* box-shadow: 10px 10px 10px 3px rgba(250, 173, 20, 0.6); */
transform: translateX(-2px);
/* transform: translateX(-2px); */
box-shadow: 1px 4px 10px rgba(0, 0, 0, 0.08);
}
@@ -294,8 +294,10 @@
}
.status-success {
background-color: #f6ffed;
/* background-color: #f6ffed; */
background-color: #e7ffcd;
color: #52c41a;
/* color: #143503; */
}
.status-error {
@@ -304,7 +306,7 @@
}
.status-warning {
background-color: #fffbe6;
background-color: #fff2aec7;
color: #faad14;
}
File diff suppressed because it is too large Load Diff