完成智慧法务前端调整20250522,还有登录和主页需要完善
This commit is contained in:
@@ -69,10 +69,11 @@ interface FilePreviewProps {
|
||||
reviewPoints?: ReviewPoint[]; // 设为可选
|
||||
activeReviewPointResultId: string | null;
|
||||
targetPage?: number; // 新增目标页码参数
|
||||
isStructuredView?: boolean; // 是否显示结构化视图
|
||||
}
|
||||
|
||||
// export function FilePreview({ fileContent, reviewPoints, activeReviewPointResultId, targetPage }: FilePreviewProps) {
|
||||
export function FilePreview({ fileContent, activeReviewPointResultId, targetPage }: FilePreviewProps) {
|
||||
export function FilePreview({ fileContent, activeReviewPointResultId, targetPage, isStructuredView = false }: FilePreviewProps) {
|
||||
const [zoomLevel, setZoomLevel] = useState(100);
|
||||
// const [highlightsVisible, setHighlightsVisible] = useState(true);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
@@ -173,6 +174,9 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
||||
// 处理页面跳转
|
||||
const prevTargetPageRef = useRef<number | undefined>(undefined);
|
||||
useEffect(() => {
|
||||
// 调试信息:记录组件状态
|
||||
// console.log(`FilePreview更新 - isStructuredView:${isStructuredView}, targetPage:${targetPage}, activeReviewPointResultId:${activeReviewPointResultId}, numPages:${numPages}`);
|
||||
|
||||
// 如果有目标页码,并且与上次相同,提示用户
|
||||
if(targetPage && numPages && targetPage <= numPages && targetPage === prevTargetPageRef.current){
|
||||
toastService.success(`已跳转至目标页码`);
|
||||
@@ -181,9 +185,6 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
||||
if (targetPage && numPages && targetPage <= numPages && (targetPage !== prevTargetPageRef.current || activeReviewPointResultId)) {
|
||||
prevTargetPageRef.current = targetPage;
|
||||
let newTargetPage = targetPage;
|
||||
// let newTargetPage = targetPage;
|
||||
// console.log("targetPage:", targetPage);
|
||||
// console.log("fileContent:", fileContent);
|
||||
|
||||
// 页码偏移量
|
||||
try {
|
||||
@@ -197,13 +198,18 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
||||
toastService.error("访问ocrResult时出错:" + (error instanceof Error ? error.message : '未知错误'));
|
||||
}
|
||||
|
||||
const pageElement = document.getElementById(`page-${newTargetPage}`);
|
||||
const pageElementId = `page-${newTargetPage}${isStructuredView ? '-structured' : ''}`;
|
||||
// console.log(`尝试跳转到元素ID: ${pageElementId}`);
|
||||
|
||||
const pageElement = document.getElementById(pageElementId);
|
||||
if (pageElement) {
|
||||
console.log(`跳转到第${newTargetPage}页,对应评查点结果ID: ${activeReviewPointResultId}`);
|
||||
pageElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
} else {
|
||||
console.warn(`未找到页面元素: ${pageElementId}`);
|
||||
}
|
||||
}
|
||||
}, [targetPage, numPages, fileContent, activeReviewPointResultId]);
|
||||
}, [targetPage, numPages, fileContent, activeReviewPointResultId, isStructuredView]);
|
||||
|
||||
// 获取评查点对应的样式类
|
||||
// const getHighlightClass = (status: string) => {
|
||||
@@ -302,9 +308,12 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
||||
marginBottom: `${calculatePageMargin(zoomFactor)}px`, // 动态计算页面间距
|
||||
};
|
||||
|
||||
// 为结构化视图和普通视图创建不同的ID
|
||||
const pageId = isStructuredView ? `page-${i}-structured` : `page-${i}`;
|
||||
|
||||
// 为每一页创建组件
|
||||
pages.push(
|
||||
<div key={i} id={`page-${i}`} style={pageContainerStyle}>
|
||||
<div key={i} id={pageId} style={pageContainerStyle}>
|
||||
{/* 页码标识,显示在页面上方 */}
|
||||
<div className="text-center text-gray-500 text-sm mb-2">第 {i} 页</div>
|
||||
|
||||
@@ -365,7 +374,20 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
||||
|
||||
// 渲染文档内容
|
||||
const renderDocumentContent = () => {
|
||||
return (
|
||||
// 如果路径无效,显示错误信息
|
||||
if (!fileContent.path) {
|
||||
return (
|
||||
<div className="text-red-500 p-4">
|
||||
<p>无法加载文件:路径无效</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 获取文件扩展名
|
||||
const fileExtension = fileContent.path.split('.').pop()?.toLowerCase();
|
||||
|
||||
// PDF内容渲染
|
||||
const renderPdfContent = () => (
|
||||
<div
|
||||
style={{
|
||||
...styles.pdfContainer,
|
||||
@@ -390,32 +412,75 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
||||
</Document>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 结构化数据渲染
|
||||
const renderStructuredData = () => (
|
||||
<div className="structured-view p-4 text-left mt-4 border-t border-gray-200">
|
||||
<div className="text-xs font-medium mb-2">结构化数据:</div>
|
||||
{fileContent.ocrResult ? (
|
||||
<div className="overflow-auto max-h-[300px]">
|
||||
<pre className="text-xs whitespace-pre-wrap bg-gray-50 p-3 rounded">
|
||||
{JSON.stringify(fileContent.ocrResult, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-gray-500 p-4 text-center">
|
||||
<p>无结构化数据可显示</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
// 根据文件类型选择不同的渲染方式
|
||||
if (fileExtension === 'pdf') {
|
||||
// 结构化视图模式:显示PDF和结构化数据
|
||||
if (isStructuredView) {
|
||||
return (
|
||||
<div>
|
||||
{renderPdfContent()}
|
||||
{renderStructuredData()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// 普通模式:仅显示PDF
|
||||
return renderPdfContent();
|
||||
} else {
|
||||
// 非PDF文件显示不支持消息
|
||||
return (
|
||||
<div className="text-gray-500 p-4">
|
||||
<p>暂不支持预览此类型的文件:{fileExtension}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="file-preview">
|
||||
<div className="file-preview-header py-2 px-4">
|
||||
<div className="file-preview-header py-2 px-4 text-xs sm:text-xs md:text-sm max-w-full text-overflow-ellipsis">
|
||||
<div className="flex items-center">
|
||||
<i className="ri-file-text-line text-primary mr-2"></i>
|
||||
<span className="font-medium text-primary">文件预览</span>
|
||||
<i className={`${isStructuredView ? 'ri-file-list-line' : 'ri-file-text-line'} text-primary mr-2`}></i>
|
||||
<span className="font-medium text-primary">{isStructuredView ? '附件预览' : '文件预览'}</span>
|
||||
</div>
|
||||
<div className="file-preview-actions flex items-center">
|
||||
<button
|
||||
className="ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs h-5 leading-5 "
|
||||
className="ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs max-h-6 leading-5"
|
||||
onClick={handleScrollToTop}
|
||||
title="返回顶部"
|
||||
>
|
||||
<i className="ri-arrow-up-double-line"></i>
|
||||
<span className="ml-1">返回顶部</span>
|
||||
<span className="ml-1 sm:inline md:inline lg:hidden xl:inline">返回顶部</span>
|
||||
</button>
|
||||
<button
|
||||
className="ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs h-5 leading-5"
|
||||
className="ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs max-h-6 leading-5"
|
||||
onClick={handleZoomIn}
|
||||
title="放大"
|
||||
>
|
||||
<i className="ri-zoom-in-line"></i>
|
||||
</button>
|
||||
<button
|
||||
className="ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs h-5 leading-5"
|
||||
className="ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs max-h-6 leading-5"
|
||||
onClick={handleZoomOut}
|
||||
title="缩小"
|
||||
>
|
||||
<i className="ri-zoom-out-line"></i>
|
||||
</button>
|
||||
@@ -423,7 +488,7 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
||||
<div className="inline-flex items-center ml-2">
|
||||
<input
|
||||
type="text"
|
||||
className="ant-input ant-input-sm py-0 px-1 text-xs h-5 leading-5 w-10 text-center
|
||||
className="ant-input ant-input-sm py-0 px-1 text-xs max-h-6 leading-5 max-w-[2.5rem] text-center
|
||||
focus:outline-none focus:ring-1 focus:ring-green-900"
|
||||
placeholder="页码"
|
||||
value={pageInputValue}
|
||||
@@ -431,29 +496,24 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
||||
onKeyDown={handlePageInputKeyDown}
|
||||
/>
|
||||
<button
|
||||
className="ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs h-5 leading-5 ml-1"
|
||||
className="ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs max-h-6 leading-5 ml-1"
|
||||
onClick={handlePageJump}
|
||||
disabled={!numPages}
|
||||
title="跳转到页面"
|
||||
>
|
||||
<i className="ri-arrow-right-line"></i>
|
||||
</button>
|
||||
{numPages && <span className="ml-1 text-xs text-gray-500">/ {numPages}</span>}
|
||||
{numPages && <span className="ml-1 text-xs text-gray-500 sm:inline md:inline lg:hidden xl:inline">/ {numPages}</span>}
|
||||
</div>
|
||||
{/* <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> */}
|
||||
<span className="ml-2 text-xs text-gray-500 inline-block">{"比例:"+zoomLevel+"%"}</span>
|
||||
<span className="ml-2 text-xs text-gray-500 inline-block sm:inline md:inline lg:hidden xl:inline">{"比例:"+zoomLevel+"%"}</span>
|
||||
<button
|
||||
className={`ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs h-5 leading-5 ml-2 ${dragMode ? 'active bg-green-300' : ''}`}
|
||||
className={`ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs max-h-6 leading-5 ml-2 ${dragMode ? 'active bg-green-300' : ''}`}
|
||||
title="切换拖拽模式"
|
||||
aria-pressed={dragMode}
|
||||
onClick={toggleDragMode}
|
||||
>
|
||||
<i className="ri-drag-move-line"></i>
|
||||
<span className="ml-1">拖拽模式{dragMode ? '(已激活)' : ''}</span>
|
||||
<span className="ml-1 sm:inline md:inline lg:hidden xl:inline">拖拽模式{dragMode ? '(已激活)' : ''}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -55,6 +55,16 @@ export interface ReviewPoint {
|
||||
};
|
||||
postAction?: string;
|
||||
actionContent?: string;
|
||||
evaluationConfig?: {
|
||||
rules?: Array<{
|
||||
type: string;
|
||||
config?: {
|
||||
fields?: string[];
|
||||
pairs?: Array<{ sourceField?: string; targetField?: string }>;
|
||||
logic?: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
// 统计数据类型
|
||||
@@ -444,84 +454,160 @@ export function ReviewPointsList({
|
||||
* @returns 评查点主要内容组件
|
||||
*/
|
||||
const renderContent = (reviewPoint: ReviewPoint, result?: boolean) => {
|
||||
// 获取evaluationConfig中type为consistency的规则 评查点一致性规则组的规则
|
||||
const consistencyRules = reviewPoint.evaluationConfig?.rules?.filter(rule => rule.type === 'consistency') || [];
|
||||
|
||||
// 获取所有consistency规则中的fields
|
||||
const allConsistencyFields: string[][] = [];
|
||||
consistencyRules.forEach(rule => {
|
||||
if (rule.config?.fields) {
|
||||
allConsistencyFields.push(rule.config.fields);
|
||||
}else if (rule.config?.pairs) {
|
||||
// 处理pairs情况,提取sourceField和targetField
|
||||
const fields: string[] = [];
|
||||
rule.config.pairs.forEach(pair => {
|
||||
if (pair.sourceField) fields.push(pair.sourceField);
|
||||
if (pair.targetField) fields.push(pair.targetField);
|
||||
});
|
||||
if (fields.length > 0) {
|
||||
allConsistencyFields.push(fields);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 对content进行排序
|
||||
const contentEntries = Object.entries(reviewPoint.content);
|
||||
|
||||
// 按照consistency规则分组
|
||||
const groupedContent: Record<string, Array<[string, { page?: number | string, value?: object }]>> = {
|
||||
'default': [] // 默认组,存放不属于任何consistency规则的项
|
||||
};
|
||||
|
||||
// 为每个consistency规则创建分组
|
||||
allConsistencyFields.forEach((fields, index) => {
|
||||
groupedContent[`consistency_${index}`] = [];
|
||||
});
|
||||
|
||||
// 将content按照规则分组
|
||||
contentEntries.forEach(entry => {
|
||||
const [key, value] = entry;
|
||||
|
||||
// 检查是否属于某个consistency规则
|
||||
let assigned = false;
|
||||
allConsistencyFields.forEach((fields, index) => {
|
||||
if (fields.includes(key)) {
|
||||
groupedContent[`consistency_${index}`].push(entry);
|
||||
assigned = true;
|
||||
}
|
||||
});
|
||||
|
||||
// 如果不属于任何规则,放入默认组
|
||||
if (!assigned) {
|
||||
groupedContent['default'].push(entry);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 修改评查结果的结构之后,显示新的结构 */}
|
||||
{Object.entries(reviewPoint.content).map(([key, value], index) => !(result && value.value?.toString().trim() == '') && (
|
||||
<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 group"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
console.log(`单独点击${key}----`, reviewPoint);
|
||||
const valuePage = parseInt(value.page as string);
|
||||
const contentPage = parseInt(reviewPoint.contentPage?.[key] as string);
|
||||
// 检查value中的page属性是否存在,优先取value中的page
|
||||
if (valuePage > 0) {
|
||||
console.log(`存在page且不为空:单独点击${key}---------->evaluated_results内的页码:`, valuePage);
|
||||
onReviewPointSelect(reviewPoint.id, valuePage);
|
||||
{/* 渲染各个分组 */}
|
||||
{Object.entries(groupedContent).map(([groupKey, entries], groupIndex) => {
|
||||
if (entries.length === 0) return null;
|
||||
|
||||
// 非默认组添加边框
|
||||
const isDefaultGroup = groupKey === 'default';
|
||||
|
||||
return (
|
||||
<div
|
||||
key={groupKey}
|
||||
className={`mb-1 ${!isDefaultGroup ? `border border-${result ? 'green' : 'red'}-200 rounded-md` : ''}`}
|
||||
>
|
||||
{/* 分组标题,只有非默认组显示 */}
|
||||
{/* {!isDefaultGroup && (
|
||||
<div className="text-xs font-medium text-red-500 mb-2">
|
||||
规则组 {groupIndex}
|
||||
</div>
|
||||
)} */}
|
||||
|
||||
{/* 渲染组内内容 */}
|
||||
{entries.map(([key, value], index) =>
|
||||
!(result && value.value?.toString().trim() == '') && (
|
||||
<div
|
||||
key={`${groupKey}_${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 group"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
console.log(`单独点击${key}----`, reviewPoint);
|
||||
const valuePage = parseInt(value.page as string);
|
||||
const contentPage = parseInt(reviewPoint.contentPage?.[key] as string);
|
||||
// 检查value中的page属性是否存在,优先取value中的page
|
||||
if (valuePage > 0) {
|
||||
console.log(`存在page且不为空:单独点击${key}---------->evaluated_results内的页码:`, valuePage);
|
||||
onReviewPointSelect(reviewPoint.id, valuePage);
|
||||
|
||||
} else if(contentPage && contentPage > 0) {
|
||||
console.log(`存在page且为空:单独点击${key}---------->ocr_result内的页码:`, contentPage);
|
||||
onReviewPointSelect(reviewPoint.id, contentPage);
|
||||
}else {
|
||||
toastService.error(`无法找到"${key}"对应的索引内容`);
|
||||
console.log(`单独点击${key}--------没有对应页码`);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
const valuePage = parseInt(value.page as string);
|
||||
const contentPage = parseInt(reviewPoint.contentPage?.[key] as string);
|
||||
// 检查value中的page属性是否存在,优先取value中的page
|
||||
if (valuePage > 0) {
|
||||
onReviewPointSelect(reviewPoint.id, valuePage);
|
||||
} else if(contentPage && contentPage > 0) {
|
||||
onReviewPointSelect(reviewPoint.id, contentPage);
|
||||
} else {
|
||||
toastService.error(`无法找到"${key}"对应的索引内容`);
|
||||
console.log(`单独点击${key}--------没有对应页码`);
|
||||
}
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={`查看${key}内容详情`}
|
||||
onMouseLeave={(e) => {
|
||||
// 获取容器内的滚动区域元素
|
||||
const scrollContainer = e.currentTarget.querySelector('.text-container');
|
||||
if (scrollContainer) {
|
||||
// 在文本缩回之前重置滚动位置
|
||||
scrollContainer.scrollTop = 0;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* <div className="flex justify-between items-center mb-1"> */}
|
||||
<div className="flex items-center mb-1">
|
||||
<span className="text-xs pr-5">
|
||||
{key}
|
||||
</span>
|
||||
<span className={`flex-shrink-0 text-xs w-15 ${value.value?.toString().trim() ? 'text-error' : 'text-warning'}`}>
|
||||
{parseInt(value.page as string)>0 || parseInt(reviewPoint.contentPage?.[key] as string)>0 ? '' : <i title="无法找到索引内容" className="ri-alert-line text-red-500 mr-2"></i>}
|
||||
{value.value?.toString().trim() ? '' : '缺失'}
|
||||
</span>
|
||||
</div>
|
||||
} else if(contentPage && contentPage > 0) {
|
||||
console.log(`存在page且为空:单独点击${key}---------->ocr_result内的页码:`, contentPage);
|
||||
onReviewPointSelect(reviewPoint.id, contentPage);
|
||||
}else {
|
||||
toastService.error(`无法找到"${key}"对应的索引内容`);
|
||||
console.log(`单独点击${key}--------没有对应页码`);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
const valuePage = parseInt(value.page as string);
|
||||
const contentPage = parseInt(reviewPoint.contentPage?.[key] as string);
|
||||
// 检查value中的page属性是否存在,优先取value中的page
|
||||
if (valuePage > 0) {
|
||||
onReviewPointSelect(reviewPoint.id, valuePage);
|
||||
} else if(contentPage && contentPage > 0) {
|
||||
onReviewPointSelect(reviewPoint.id, contentPage);
|
||||
} else {
|
||||
toastService.error(`无法找到"${key}"对应的索引内容`);
|
||||
console.log(`单独点击${key}--------没有对应页码`);
|
||||
}
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={`查看${key}内容详情`}
|
||||
onMouseLeave={(e) => {
|
||||
// 获取容器内的滚动区域元素
|
||||
const scrollContainer = e.currentTarget.querySelector('.text-container');
|
||||
if (scrollContainer) {
|
||||
// 在文本缩回之前重置滚动位置
|
||||
scrollContainer.scrollTop = 0;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* <div className="flex justify-between items-center mb-1"> */}
|
||||
<div className="flex items-center mb-1">
|
||||
<span className="text-xs pr-5">
|
||||
{key}
|
||||
</span>
|
||||
<span className={`flex-shrink-0 text-xs w-15 ${value.value?.toString().trim() ? 'text-error' : 'text-warning'}`}>
|
||||
{parseInt(value.page as string)>0 || parseInt(reviewPoint.contentPage?.[key] as string)>0 ? '' : <i title="无法找到索引内容" className="ri-alert-line text-red-500 mr-2"></i>}
|
||||
{value.value?.toString().trim() ? '' : '缺失'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="relative text-container max-h-96 group-hover:overflow-auto overflow-hidden">
|
||||
<p
|
||||
className="text-xs text-left select-text block overflow-hidden !line-clamp-2
|
||||
group-hover:!line-clamp-none group-hover:bg-white group-hover:shadow-md group-hover:z-10 group-hover:relative px-1 rounded transition-all duration-300 ease-in-out cursor-text"
|
||||
// title={value.value?.toString() || ''}
|
||||
// style={{ userSelect: 'all' }}
|
||||
>
|
||||
{(value.value?.toString().trim() === '')
|
||||
? ""
|
||||
: value.value?.toString() || ''}
|
||||
</p>
|
||||
<div className="relative text-container max-h-96 group-hover:overflow-auto overflow-hidden">
|
||||
<p
|
||||
className="text-xs text-left select-text block overflow-hidden !line-clamp-2
|
||||
group-hover:!line-clamp-none group-hover:bg-white group-hover:shadow-md group-hover:z-10 group-hover:relative px-1 rounded transition-all duration-300 ease-in-out cursor-text"
|
||||
// title={value.value?.toString() || ''}
|
||||
// style={{ userSelect: 'all' }}
|
||||
>
|
||||
{(value.value?.toString().trim() === '')
|
||||
? ""
|
||||
: value.value?.toString() || ''}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -533,7 +619,7 @@ export function ReviewPointsList({
|
||||
* @returns 评查点内容与建议组件
|
||||
*/
|
||||
const renderReviewPointContent = (reviewPoint: ReviewPoint) => {
|
||||
|
||||
|
||||
const handleManualReviewNotesChange = (reviewPointId: string, text: string) => {
|
||||
setManualReviewNotes(prev => ({
|
||||
...prev,
|
||||
@@ -541,8 +627,9 @@ export function ReviewPointsList({
|
||||
}));
|
||||
};
|
||||
|
||||
// 如果当前评查点不处于编辑状态,只显示简单信息
|
||||
// 如果当前评查点不处于编辑状态 TODO delete
|
||||
if (editingReviewPoint !== reviewPoint.id) {
|
||||
|
||||
// 根据result和status决定渲染哪种样式
|
||||
if (reviewPoint.result === true) {
|
||||
// 已通过的评查点只显示基本信息和人工审核注释
|
||||
@@ -893,11 +980,11 @@ export function ReviewPointsList({
|
||||
>
|
||||
{/* 评查点标题和状态 */}
|
||||
{/* 评查点名称 pointName*/}
|
||||
<div className="review-point-title flex-1 text-left text-blue-500">{'评查点名称:' + reviewPoint.pointName}</div>
|
||||
<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 min-w-[25%]">{reviewPoint.title}</div>
|
||||
<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
|
||||
@@ -907,7 +994,7 @@ export function ReviewPointsList({
|
||||
>
|
||||
{reviewPoint.groupName}
|
||||
</span>
|
||||
</div>
|
||||
</div> */}
|
||||
<div className="flex flex-col items-center ml-2 flex-shrink-0 max-w-[15%]">
|
||||
{renderStatusBadge(reviewPoint.status, reviewPoint.result)}
|
||||
{renderHumanReviewBadge(reviewPoint)}
|
||||
|
||||
@@ -15,6 +15,7 @@ interface ReviewTabsProps {
|
||||
previousRoute?: string;
|
||||
path?: string;
|
||||
auditStatus?: number;
|
||||
type?: string;
|
||||
};
|
||||
onConfirmResults: () => void;
|
||||
}
|
||||
@@ -104,14 +105,16 @@ export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfi
|
||||
>
|
||||
<i className="ri-lightbulb-line"></i> AI智能分析
|
||||
</button> */}
|
||||
<button
|
||||
className={`tab-nav-item ${activeTab === 'fileinfo' ? 'active' : ''}`}
|
||||
onClick={() => onTabChange('fileinfo')}
|
||||
type="button"
|
||||
aria-pressed={activeTab === 'fileinfo'}
|
||||
{fileInfo.type === '1' && (
|
||||
<button
|
||||
className={`tab-nav-item ${activeTab === 'filecompare' ? 'active' : ''}`}
|
||||
onClick={() => onTabChange('filecompare')}
|
||||
type="button"
|
||||
aria-pressed={activeTab === 'filecompare'}
|
||||
>
|
||||
<i className="ri-flip-horizontal-line"></i> 文件比对
|
||||
</button>
|
||||
<i className="ri-flip-horizontal-line"></i> 结构比对
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className={`tab-nav-item ${activeTab === 'fileinfo' ? 'active' : ''}`}
|
||||
onClick={() => onTabChange('fileinfo')}
|
||||
|
||||
@@ -118,8 +118,27 @@ export function ExtractionSettings({
|
||||
// 自动保存字段变更状态
|
||||
// 这个效果确保添加字段后自动保存到组件状态,但不自动提交更新
|
||||
useEffect(() => {
|
||||
setHasPendingChanges(true);
|
||||
}, [fields, regexFields, promptContent])
|
||||
// 初始加载时不设置hasPendingChanges为true
|
||||
// 仅当用户进行了实际修改后才标记为有变更
|
||||
const initialLlmFields = initialData?.extraction_config?.llm?.fields || [];
|
||||
const initialVlmFields = initialData?.extraction_config?.vlm?.fields || [];
|
||||
const initialRegexFields = initialData?.extraction_config?.regex?.fields || [];
|
||||
const initialLlmPrompt = initialData?.extraction_config?.llm?.prompt_setting?.template || '';
|
||||
const initialVlmPrompt = initialData?.extraction_config?.vlm?.prompt_setting?.template || '';
|
||||
|
||||
// 检查是否有实际变化
|
||||
const hasLlmFieldsChanged = JSON.stringify(fields.llm) !== JSON.stringify(initialLlmFields);
|
||||
const hasVlmFieldsChanged = JSON.stringify(fields.vlm) !== JSON.stringify(initialVlmFields);
|
||||
const hasRegexFieldsChanged = JSON.stringify(regexFields) !== JSON.stringify(initialRegexFields);
|
||||
const hasPromptContentChanged =
|
||||
promptContent.llm !== initialLlmPrompt ||
|
||||
promptContent.vlm !== initialVlmPrompt;
|
||||
|
||||
// 只有实际发生变化时才设置为true
|
||||
if (hasLlmFieldsChanged || hasVlmFieldsChanged || hasRegexFieldsChanged || hasPromptContentChanged) {
|
||||
setHasPendingChanges(true);
|
||||
}
|
||||
}, [fields, regexFields, promptContent, initialData])
|
||||
|
||||
// 处理字段输入变化
|
||||
const handleFieldInputChange = (
|
||||
@@ -541,7 +560,7 @@ export function ExtractionSettings({
|
||||
<label className="form-label mb-1" htmlFor="field-input">
|
||||
抽取字段
|
||||
</label>
|
||||
<div className="flex mb-2">
|
||||
<div className="flex">
|
||||
<input
|
||||
type="text"
|
||||
className="form-input mr-2"
|
||||
@@ -561,6 +580,9 @@ export function ExtractionSettings({
|
||||
添加
|
||||
</button>
|
||||
</div>
|
||||
<div className="form-tip mb-2 text-xs">
|
||||
支持一次输入多个字段,用逗号、空格或顿号分隔
|
||||
</div>
|
||||
<div className="chips-container" id="fields-container">
|
||||
{fields.llm.map((field, index) => (
|
||||
<div className="chip" key={`llm-field-${index}`}>
|
||||
@@ -581,9 +603,7 @@ export function ExtractionSettings({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="form-tip mt-1 text-xs">
|
||||
支持一次输入多个字段,用逗号、空格或顿号分隔
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -666,11 +686,18 @@ export function ExtractionSettings({
|
||||
].map((variable) => (
|
||||
<button
|
||||
key={variable}
|
||||
type="button"
|
||||
type="button"
|
||||
className="var-tag"
|
||||
onClick={() => applyVariableToPrompt(variable, "llm")}
|
||||
>
|
||||
{variable}
|
||||
{variable=='docType' ? '文档类型:{docType}':
|
||||
variable=='fieldsList' ? '抽取字段列表:{fieldsList}':
|
||||
variable=='companyName' ? '公司名称:{companyName}':
|
||||
variable=='documentId' ? '文档编号:{documentId}':
|
||||
variable=='date' ? '日期:{date}':
|
||||
variable=='industry' ? '行业:{industry}':
|
||||
variable=='ocrText' ? 'OCR文本:{ocrText}':
|
||||
variable}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -692,7 +719,7 @@ export function ExtractionSettings({
|
||||
<label className="form-label mb-1" htmlFor="field-input-vlm">
|
||||
抽取字段与类型
|
||||
</label>
|
||||
<div className="flex mb-2">
|
||||
<div className="flex">
|
||||
<input
|
||||
type="text"
|
||||
className="form-input mr-2"
|
||||
@@ -713,6 +740,9 @@ export function ExtractionSettings({
|
||||
添加
|
||||
</button>
|
||||
</div>
|
||||
<div className="form-tip mb-2 text-xs">
|
||||
请为每个字段选择适当的抽取类型,有助于提高识别准确率
|
||||
</div>
|
||||
<div className="chips-container" id="fields-container-vlm">
|
||||
{fields.vlm.map((field, index) => {
|
||||
const { fieldName, fieldType, typeName, badgeClass } =
|
||||
@@ -747,9 +777,7 @@ export function ExtractionSettings({
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="form-tip mt-1 text-xs">
|
||||
请为每个字段选择适当的抽取类型,有助于提高识别准确率
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -843,7 +871,17 @@ export function ExtractionSettings({
|
||||
className="var-tag"
|
||||
onClick={() => applyVariableToPrompt(variable, "vlm")}
|
||||
>
|
||||
{variable}
|
||||
{variable=='docType' ? '文档类型:{docType}':
|
||||
variable=='fieldsList' ? '抽取字段列表:{fieldsList}':
|
||||
variable=='companyName' ? '公司名称:{companyName}':
|
||||
variable=='documentId' ? '文档编号:{documentId}':
|
||||
variable=='date' ? '日期:{date}':
|
||||
variable=='industry' ? '行业:{industry}':
|
||||
variable=='contentType' ? '内容类型:{contentType}':
|
||||
variable=='pageRange' ? '页面范围:{pageRange}':
|
||||
variable=='colorMode' ? '色彩模式:{colorMode}':
|
||||
variable=='ocrText' ? 'OCR文本:{ocrText}':
|
||||
variable}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface DateRangePickerProps {
|
||||
endId?: string;
|
||||
format?: string;
|
||||
placeholder?: string;
|
||||
colorMode?: 'light' | 'dark' | 'auto';
|
||||
}
|
||||
|
||||
export function links() {
|
||||
@@ -57,7 +58,8 @@ export function DateRangePicker({
|
||||
startId = "date-start",
|
||||
endId = "date-end",
|
||||
format = "yyyy-MM-dd",
|
||||
placeholder = "请选择时间"
|
||||
placeholder = "请选择时间",
|
||||
colorMode = 'auto'
|
||||
}: DateRangePickerProps) {
|
||||
// 使用ref获取input元素
|
||||
const startInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -121,8 +123,15 @@ export function DateRangePicker({
|
||||
setFocusedInput(null);
|
||||
};
|
||||
|
||||
// 获取颜色模式类名
|
||||
const getColorModeClass = () => {
|
||||
if (colorMode === 'light') return 'color-mode-light';
|
||||
if (colorMode === 'dark') return 'color-mode-dark';
|
||||
return ''; // auto模式不添加额外类名
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`date-range-picker ${className}`}>
|
||||
<div className={`date-range-picker ${getColorModeClass()} ${className}`}>
|
||||
<div className="date-range-fields">
|
||||
<div className="date-field">
|
||||
<label htmlFor={startId} className="date-label">{startLabel}</label>
|
||||
@@ -200,7 +209,8 @@ export function SimpleDateRangePicker({
|
||||
startId = "date-start-simple",
|
||||
endId = "date-end-simple",
|
||||
format = "yyyy-MM-dd",
|
||||
placeholder = "请选择时间"
|
||||
placeholder = "请选择时间",
|
||||
colorMode = 'auto'
|
||||
}: Omit<DateRangePickerProps, 'startLabel' | 'endLabel'>) {
|
||||
// 使用ref获取input元素
|
||||
const startInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -264,8 +274,15 @@ export function SimpleDateRangePicker({
|
||||
setFocusedInput(null);
|
||||
};
|
||||
|
||||
// 获取颜色模式类名
|
||||
const getColorModeClass = () => {
|
||||
if (colorMode === 'light') return 'color-mode-light';
|
||||
if (colorMode === 'dark') return 'color-mode-dark';
|
||||
return ''; // auto模式不添加额外类名
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`date-range-picker simple-date-range-picker ${className}`}>
|
||||
<div className={`date-range-picker simple-date-range-picker ${getColorModeClass()} ${className}`}>
|
||||
<div className="date-range-fields">
|
||||
<div
|
||||
className={`date-input-wrapper ${focusedInput === startId ? 'focused' : ''}`}
|
||||
|
||||
@@ -6,6 +6,10 @@ export type FileType =
|
||||
| 'license'
|
||||
| 'punishment'
|
||||
| 'agreement'
|
||||
| 'contract' // 合同文档
|
||||
| 'license-doc' // 行政许可卷宗
|
||||
| 'punishment-doc' // 行政处罚卷宗
|
||||
| 'other' // 其他
|
||||
| string;
|
||||
|
||||
interface FileTypeTagProps {
|
||||
@@ -15,6 +19,8 @@ interface FileTypeTagProps {
|
||||
size?: 'default' | 'sm' | 'lg';
|
||||
showIcon?: boolean;
|
||||
fileType?: string;
|
||||
colorMode?: 'light' | 'dark' | 'auto';
|
||||
typeName?: string; // 新增typeName属性,用于根据文档类型名称判断样式
|
||||
}
|
||||
|
||||
export function links() {
|
||||
@@ -30,6 +36,8 @@ export function links() {
|
||||
* @param size 尺寸:default, sm, lg
|
||||
* @param showIcon 是否显示图标,默认为true
|
||||
* @param fileType 文件类型,不提供则使用文档类型决定样式
|
||||
* @param colorMode 颜色模式:light, dark, auto(默认),设置为auto时跟随系统
|
||||
* @param typeName 文档类型名称,用于判断样式
|
||||
*/
|
||||
export function FileTypeTag({
|
||||
type,
|
||||
@@ -37,7 +45,9 @@ export function FileTypeTag({
|
||||
className = '',
|
||||
size = 'default',
|
||||
showIcon = true,
|
||||
fileType
|
||||
fileType,
|
||||
colorMode = 'auto',
|
||||
typeName
|
||||
}: FileTypeTagProps) {
|
||||
// 文档类型对应的图标
|
||||
const getTypeIcon = () => {
|
||||
@@ -47,26 +57,75 @@ export function FileTypeTag({
|
||||
'license': 'ri-vip-crown-line',
|
||||
'punishment': 'ri-scales-line',
|
||||
'agreement': 'ri-file-paper-line',
|
||||
'pdf': 'ri-file-pdf-line',
|
||||
'doc': 'ri-file-word-line',
|
||||
'docx': 'ri-file-word-line',
|
||||
'xls': 'ri-file-excel-line',
|
||||
'xlsx': 'ri-file-excel-line',
|
||||
'ppt': 'ri-file-ppt-line',
|
||||
'pptx': 'ri-file-ppt-line',
|
||||
'contract': 'ri-file-list-3-line', // 合同文档
|
||||
'license-doc': 'ri-vip-crown-line', // 行政许可卷宗
|
||||
'punishment-doc': 'ri-scales-line', // 行政处罚卷宗
|
||||
'other': 'ri-file-paper-line', // 其他
|
||||
};
|
||||
return typeIconMap[type] || 'ri-file-text-line';
|
||||
};
|
||||
|
||||
// 文档类型对应的文本
|
||||
const getTypeText = () => {
|
||||
// 如果提供了自定义文本,优先使用
|
||||
if (text) return text;
|
||||
|
||||
// 如果提供了typeName,优先使用
|
||||
if (typeName) return typeName;
|
||||
|
||||
const typeTextMap: Record<string, string> = {
|
||||
'sales-contract': '销售合同',
|
||||
'purchase-contract': '采购合同',
|
||||
'license': '专卖许可证',
|
||||
'punishment': '行政处罚决定书',
|
||||
'agreement': '承包协议',
|
||||
'pdf': 'PDF',
|
||||
'doc': 'Word',
|
||||
'docx': 'Word',
|
||||
'xls': 'Excel',
|
||||
'xlsx': 'Excel',
|
||||
'ppt': 'PPT',
|
||||
'contract': '合同文档', // 合同文档
|
||||
'license-doc': '行政许可卷宗', // 行政许可卷宗
|
||||
'punishment-doc': '行政处罚卷宗', // 行政处罚卷宗
|
||||
'other': '其他文档', // 其他
|
||||
};
|
||||
return typeTextMap[type] || type;
|
||||
};
|
||||
|
||||
// 获取根据typeName判断的样式类名
|
||||
const getTypeNameClass = () => {
|
||||
if (!typeName) return '';
|
||||
|
||||
// 根据typeName判断文档类型
|
||||
if (typeName.includes('合同')) {
|
||||
return 'file-type-tag-contract';
|
||||
} else if (typeName.includes('许可') || typeName.includes('行政许可')) {
|
||||
return 'file-type-tag-license-doc';
|
||||
} else if (typeName.includes('处罚') || typeName.includes('行政处罚')) {
|
||||
return 'file-type-tag-punishment-doc';
|
||||
} else {
|
||||
return 'file-type-tag-other';
|
||||
}
|
||||
};
|
||||
|
||||
// 获取文档类型对应的类名
|
||||
const getTypeClass = () => {
|
||||
// 如果有typeName,根据typeName判断样式
|
||||
if (typeName) {
|
||||
const typeNameClass = getTypeNameClass();
|
||||
if (typeNameClass) {
|
||||
return `file-type-tag ${typeNameClass}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有文件类型,优先使用文件类型决定样式
|
||||
if (fileType) {
|
||||
const fileTypeClass = getFileTypeClass(fileType);
|
||||
@@ -108,8 +167,15 @@ export function FileTypeTag({
|
||||
return !showIcon ? 'file-type-tag-no-icon' : '';
|
||||
};
|
||||
|
||||
// 获取颜色模式类名
|
||||
const getColorModeClass = () => {
|
||||
if (colorMode === 'light') return 'color-mode-light';
|
||||
if (colorMode === 'dark') return 'color-mode-dark';
|
||||
return ''; // auto模式不添加额外类名
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={`${getTypeClass()} ${getSizeClass()} ${getIconClass()} ${className}`}>
|
||||
<span className={`${getTypeClass()} ${getSizeClass()} ${getIconClass()} ${getColorModeClass()} ${className}`}>
|
||||
{showIcon && <i className={`${getTypeIcon()} file-type-icon`}></i>}
|
||||
<span className="file-type-text">{getTypeText()}</span>
|
||||
</span>
|
||||
|
||||
@@ -143,6 +143,7 @@ interface DateRangeFilterProps {
|
||||
startLabel?: string;
|
||||
endLabel?: string;
|
||||
simple?: boolean;
|
||||
colorMode?: 'light' | 'dark' | 'auto';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,7 +169,8 @@ export function DateRangeFilter({
|
||||
className = '',
|
||||
startLabel = "从",
|
||||
endLabel = "至",
|
||||
simple = false
|
||||
simple = false,
|
||||
colorMode = 'auto'
|
||||
}: DateRangeFilterProps) {
|
||||
return (
|
||||
<div className={`filter-item ${className}`}>
|
||||
@@ -180,6 +182,7 @@ export function DateRangeFilter({
|
||||
onStartDateChange={onStartDateChange}
|
||||
onEndDateChange={onEndDateChange}
|
||||
className="filter-control"
|
||||
colorMode={colorMode}
|
||||
/>
|
||||
) : (
|
||||
<DateRangePicker
|
||||
@@ -190,6 +193,7 @@ export function DateRangeFilter({
|
||||
startLabel={startLabel}
|
||||
endLabel={endLabel}
|
||||
className="filter-control"
|
||||
colorMode={colorMode}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user