311 lines
12 KiB
TypeScript
311 lines
12 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
|
|
// 定义字段比对结果类型
|
|
interface FieldComparison {
|
|
field: string;
|
|
status: string;
|
|
details: string;
|
|
source_page: string;
|
|
template_page: string;
|
|
}
|
|
|
|
// 定义比对结果类型
|
|
interface ComparisonResults {
|
|
[sectionName: string]: FieldComparison[];
|
|
}
|
|
|
|
// 定义比对文档类型
|
|
interface ComparisonDocument {
|
|
comparison_results?: ComparisonResults;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
// 定义组件Props类型
|
|
interface ComparisonProps {
|
|
comparison_document: ComparisonDocument | null;
|
|
onPageJump?: (sourcePage: number, templatePage: number) => void;
|
|
}
|
|
|
|
// 筛选类型
|
|
type FilterType = 'all' | 'normal' | 'abnormal';
|
|
|
|
export function Comparison({ comparison_document, onPageJump }: ComparisonProps) {
|
|
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set());
|
|
const [filterType, setFilterType] = useState<FilterType>('all');
|
|
|
|
// 默认展开所有章节
|
|
useEffect(() => {
|
|
if (comparison_document?.comparison_results) {
|
|
const allSections = Object.keys(comparison_document.comparison_results);
|
|
setExpandedSections(new Set(allSections));
|
|
}
|
|
}, [comparison_document?.comparison_results]);
|
|
|
|
// 如果没有比对文档,显示暂无数据
|
|
if (!comparison_document || !comparison_document.comparison_results) {
|
|
return (
|
|
<div className="bg-white rounded-lg border border-gray-200 h-[calc(100vh-120px)] overflow-y-auto">
|
|
<div className="review-panel-header py-2 px-4 flex items-center">
|
|
<i className="ri-file-compare-line text-primary mr-2"></i>
|
|
<span className="font-medium text-primary">结构比对(0)</span>
|
|
</div>
|
|
<div className="text-center py-8 text-gray-500">
|
|
<i className="ri-file-search-line text-4xl mb-2 block"></i>
|
|
<p>暂无结构比对数据</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const comparisonResults = comparison_document.comparison_results;
|
|
|
|
// 确保 comparisonResults 的所有值都是数组,过滤掉非数组项
|
|
const validComparisonResults: ComparisonResults = {};
|
|
if (comparisonResults) {
|
|
Object.entries(comparisonResults).forEach(([key, value]) => {
|
|
if (Array.isArray(value)) {
|
|
validComparisonResults[key] = value;
|
|
}
|
|
});
|
|
}
|
|
|
|
// 切换章节展开状态
|
|
const toggleSection = (sectionName: string) => {
|
|
const newExpanded = new Set(expandedSections);
|
|
if (newExpanded.has(sectionName)) {
|
|
newExpanded.delete(sectionName);
|
|
} else {
|
|
newExpanded.add(sectionName);
|
|
}
|
|
setExpandedSections(newExpanded);
|
|
};
|
|
|
|
// 处理整个字段框的点击,同时传递两个页码
|
|
const handleFieldClick = (field: FieldComparison) => {
|
|
if (onPageJump) {
|
|
const sourcePage = field.source_page ? parseInt(field.source_page) : 0;
|
|
const templatePage = field.template_page ? parseInt(field.template_page) : 0;
|
|
onPageJump(sourcePage, templatePage);
|
|
}
|
|
};
|
|
|
|
// 处理键盘事件
|
|
const handleFieldKeyDown = (event: React.KeyboardEvent, field: FieldComparison) => {
|
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
event.preventDefault();
|
|
handleFieldClick(field);
|
|
}
|
|
};
|
|
|
|
// 获取状态样式
|
|
const getStatusStyle = (status: string) => {
|
|
if (status === 'normal') {
|
|
return {
|
|
icon: 'ri-check-circle-line',
|
|
color: 'text-green-500',
|
|
bgColor: 'bg-green-50',
|
|
borderColor: 'border-green-200'
|
|
};
|
|
} else {
|
|
return {
|
|
icon: 'ri-alert-circle-line',
|
|
color: 'text-red-500',
|
|
bgColor: 'bg-red-50',
|
|
borderColor: 'border-red-200'
|
|
};
|
|
}
|
|
};
|
|
|
|
// 统计总体信息
|
|
const totalFields = Object.values(validComparisonResults).flat().length;
|
|
const normalFields = Object.values(validComparisonResults).flat().filter(field => field.status === 'normal').length;
|
|
const abnormalFields = totalFields - normalFields;
|
|
|
|
// 根据筛选类型过滤数据
|
|
const getFilteredResults = () => {
|
|
if (filterType === 'all') {
|
|
return validComparisonResults;
|
|
}
|
|
|
|
const filteredResults: ComparisonResults = {};
|
|
|
|
Object.entries(validComparisonResults).forEach(([sectionName, fields]) => {
|
|
let filteredFields: FieldComparison[] = [];
|
|
|
|
if (filterType === 'normal') {
|
|
filteredFields = fields.filter(field => field.status === 'normal');
|
|
} else if (filterType === 'abnormal') {
|
|
filteredFields = fields.filter(field => field.status !== 'normal');
|
|
}
|
|
|
|
if (filteredFields.length > 0) {
|
|
filteredResults[sectionName] = filteredFields;
|
|
}
|
|
});
|
|
|
|
return filteredResults;
|
|
};
|
|
|
|
const filteredResults = getFilteredResults();
|
|
|
|
return (
|
|
<div className="bg-white rounded-lg border border-gray-200 h-[calc(100vh-120px)] flex flex-col">
|
|
{/* 固定头部 */}
|
|
<div className="flex-shrink-0 text-sm font-medium">
|
|
<div className="review-panel-header py-2 px-4 flex items-center border-b border-gray-100">
|
|
<i className="ri-book-read-line text-primary mr-2"></i>
|
|
<span className="font-medium text-primary">结构比对({totalFields})</span>
|
|
</div>
|
|
|
|
{/* 固定统计概览和筛选按钮 */}
|
|
<div className="p-4 border-b border-gray-200">
|
|
{/* 统计概览 */}
|
|
<div className="grid grid-cols-2 gap-2 text-sm mb-2">
|
|
<button
|
|
onClick={() => setFilterType('normal')}
|
|
className={`text-center py-2 rounded flex items-center justify-center transition-all duration-200 ${
|
|
filterType === 'normal'
|
|
? 'bg-green-100 border-2 border-green-300'
|
|
: 'bg-green-50 border-2 border-transparent hover:bg-green-100'
|
|
}`}
|
|
>
|
|
<div className="text-green-600 mr-2">正常</div>
|
|
<div className="font-medium text-green-600">{normalFields}</div>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => setFilterType('abnormal')}
|
|
className={`text-center py-2 rounded flex items-center justify-center transition-all duration-200 ${
|
|
filterType === 'abnormal'
|
|
? 'bg-red-100 border-2 border-red-300'
|
|
: 'bg-red-50 border-2 border-transparent hover:bg-red-100'
|
|
}`}
|
|
>
|
|
<div className="text-red-600 mr-2">异常</div>
|
|
<div className="font-medium text-red-600">{abnormalFields}</div>
|
|
</button>
|
|
</div>
|
|
|
|
{/* 筛选重置按钮 */}
|
|
{filterType !== 'all' && (
|
|
<button
|
|
onClick={() => setFilterType('all')}
|
|
className="w-full text-center py-1 text-xs text-gray-600 hover:text-gray-800 border border-gray-200 rounded hover:bg-gray-50 transition-colors"
|
|
>
|
|
显示全部 ({totalFields})
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 滚动内容区域 */}
|
|
<div className="flex-1 overflow-y-auto">
|
|
{Object.keys(filteredResults).length === 0 ? (
|
|
<div className="text-center py-8 text-gray-500">
|
|
<i className="ri-filter-off-line text-2xl mb-2 block"></i>
|
|
<p>没有找到{filterType === 'normal' ? '正常' : '异常'}的字段</p>
|
|
</div>
|
|
) : (
|
|
Object.entries(filteredResults).map(([sectionName, fields]) => {
|
|
const isExpanded = expandedSections.has(sectionName);
|
|
const sectionAbnormalCount = fields.filter(field => field.status !== 'normal').length;
|
|
|
|
return (
|
|
<div key={sectionName} className="border-b border-gray-100 last:border-b-0">
|
|
{/* 章节头部 */}
|
|
<button
|
|
onClick={() => toggleSection(sectionName)}
|
|
className="w-full px-4 py-3 text-left hover:bg-gray-50 flex items-center justify-between transition-colors"
|
|
>
|
|
<div className="flex items-center">
|
|
<i className={`${isExpanded ? 'ri-arrow-down-s-line' : 'ri-arrow-right-s-line'} mr-2 text-gray-400 transition-transform`}></i>
|
|
<span className="font-medium text-gray-900">{sectionName}</span>
|
|
<span className="ml-2 text-sm text-gray-500">({fields.length})</span>
|
|
{sectionAbnormalCount > 0 && filterType === 'all' && (
|
|
<span className="ml-2 px-2 py-0.5 bg-red-100 text-red-600 text-xs rounded-full">
|
|
{sectionAbnormalCount}异常
|
|
</span>
|
|
)}
|
|
</div>
|
|
</button>
|
|
|
|
{/* 章节内容 */}
|
|
{isExpanded && (
|
|
<div className="px-4 pb-3">
|
|
{fields.map((field, index) => {
|
|
const statusStyle = getStatusStyle(field.status);
|
|
|
|
return (
|
|
<div
|
|
key={index}
|
|
role="button"
|
|
tabIndex={0}
|
|
onClick={() => handleFieldClick(field)}
|
|
onKeyDown={(e) => handleFieldKeyDown(e, field)}
|
|
className={`text-sm mb-3 last:mb-0 p-2 rounded-lg border cursor-pointer transition-all duration-200 ${statusStyle.bgColor} ${statusStyle.borderColor} hover:shadow-md hover:scale-[1.02] hover:border-opacity-80
|
|
focus:outline-none focus:ring-1 ${field.status === 'normal' ? 'focus:ring-green-700' : 'focus:ring-red-700'} focus:ring-opacity-50 ${field.status === 'normal' ? 'border-green-200' : 'border-red-200'}`}
|
|
>
|
|
{/* 字段名和状态 */}
|
|
<div className="flex items-center justify-between mb-1">
|
|
<div className="flex items-center">
|
|
<span className="font-medium text-gray-900">{field.field}</span>
|
|
</div>
|
|
<span className={`text-xs px-2 py-1 rounded-full ${
|
|
field.status === 'normal'
|
|
? 'bg-green-100 text-green-700'
|
|
: 'bg-red-100 text-red-700'
|
|
}`}>
|
|
{field.status === 'normal' ? '正常' : '异常'}
|
|
</span>
|
|
</div>
|
|
|
|
{/* 详细说明 */}
|
|
<p className="text-sm text-gray-600 mb-3">{field.details}</p>
|
|
|
|
{/* 页码信息 */}
|
|
<div className="flex items-center gap-4 text-xs">
|
|
{field.source_page ? (
|
|
<div className="flex items-center text-blue-600">
|
|
<i className="ri-file-text-line mr-1"></i>
|
|
主文件: 第{field.source_page}页
|
|
</div>
|
|
) : (
|
|
<div className="flex items-center text-gray-400">
|
|
<i className="ri-file-text-line mr-1"></i>
|
|
主文件: 未找到内容
|
|
</div>
|
|
)}
|
|
|
|
{field.template_page ? (
|
|
<div className="flex items-center text-purple-600">
|
|
<i className="ri-file-copy-line mr-1"></i>
|
|
模板: 第{field.template_page}页
|
|
</div>
|
|
) : (
|
|
<div className="flex items-center text-gray-400">
|
|
<i className="ri-file-copy-line mr-1"></i>
|
|
模板: 未找到内容
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
|
|
{/* 底部说明 */}
|
|
{/* <div className="flex-shrink-0 px-4 py-3 bg-gray-50 text-xs text-gray-500 rounded-b-lg border-t border-gray-100">
|
|
<div className="flex items-center">
|
|
<i className="ri-information-line mr-1"></i>
|
|
点击字段框可同时跳转到主文件和模板对应页面
|
|
</div>
|
|
</div> */}
|
|
</div>
|
|
);
|
|
} |