/** * 右栏 · 详情面板 * 包含 3 个选项卡(评查结果、抽取字段、文件信息)和底部操作栏 */ import type { ReviewPoint, CharPosition, PdfBboxHighlight } from '../ReviewPointsList'; import { ReviewPointDetailCard } from './ReviewPointDetailCard'; import { FileInfoPanel } from './FileInfoPanel'; type TabKey = 'result' | 'fields' | 'info'; interface FileInfoData { fileName: string; contractNumber: string; fileSize: string; fileFormat: string; pageCount: number; uploadTime: string; uploadUser: string; fileType: string; } interface ReviewInfoData { reviewTime: string; reviewModel: string; ruleGroup: string; result: string; issueCount: number; } interface DetailPanelProps { activeTab: TabKey; onTabChange: (tab: TabKey) => void; activeReviewPoint: ReviewPoint | null; reviewPoints: ReviewPoint[]; detailMode?: 'legacy' | 'leaudit'; fileInfo: FileInfoData; reviewInfo: ReviewInfoData; onReviewPointSelect: (id: string | number, page?: number, charPositions?: CharPosition[], value?: string, bboxHighlight?: PdfBboxHighlight) => void; onStatusChange: (id: string, editAuditStatusId: string | number, status: string, message: string) => void; onConfirmResults: () => void; onDownload: () => void; auditStatus?: number; fileFormat?: string; onUploadTemplate?: () => void; onComparison?: () => void; showComparisonButton?: boolean; } function isValidQuad(value: unknown): value is [number, number, number, number] { return Array.isArray(value) && value.length === 4 && value.every(item => typeof item === 'number' && Number.isFinite(item)); } function hasNonZeroQuad(value: [number, number, number, number]): boolean { return value.some(item => item !== 0); } function getFieldRawValue(value: ReviewPoint['content'][string]): unknown { if (value == null) return null; if (typeof value === 'object' && 'value' in value) { return (value as { value?: unknown }).value ?? null; } return value; } function getFieldDisplayText(rawValue: unknown): string { if (rawValue == null) return '缺失'; if (typeof rawValue === 'string' || typeof rawValue === 'number' || typeof rawValue === 'boolean') return String(rawValue); try { return JSON.stringify(rawValue); } catch { return String(rawValue); } } function getFieldHighlightText(rawValue: unknown): string | undefined { if (rawValue == null) return undefined; if (typeof rawValue !== 'string' && typeof rawValue !== 'number' && typeof rawValue !== 'boolean') return undefined; const text = String(rawValue).trim(); return text ? String(rawValue) : undefined; } function getFieldPage(point: ReviewPoint, key: string, value: ReviewPoint['content'][string]): number | undefined { const contentPage = point.contentPage?.[key]; const parsedContentPage = Number(contentPage); if (Number.isFinite(parsedContentPage) && parsedContentPage > 0) return parsedContentPage; const inlinePage = typeof value === 'object' && value && 'page' in value ? Number((value as { page?: unknown }).page) : NaN; if (Number.isFinite(inlinePage) && inlinePage > 0) return inlinePage; const pageNum = point.fieldPositions?.[key]?.page_num; if (typeof pageNum === 'number' && Number.isFinite(pageNum) && pageNum >= 0) return pageNum + 1; return undefined; } function getFieldConfidence(point: ReviewPoint, key: string): number | undefined { const confidence = point.fieldPositions?.[key]?.confidence; if (typeof confidence !== 'number' || !Number.isFinite(confidence)) return undefined; return confidence; } function getFieldBboxHighlight(point: ReviewPoint, key: string, page?: number): PdfBboxHighlight | undefined { const fieldPosition = point.fieldPositions?.[key]; if (!fieldPosition || !isValidQuad(fieldPosition.bbox) || !isValidQuad(fieldPosition.page_box)) return undefined; if (!hasNonZeroQuad(fieldPosition.bbox) || !hasNonZeroQuad(fieldPosition.page_box)) return undefined; return { fieldKey: key, bbox: [...fieldPosition.bbox], pageBox: [...fieldPosition.page_box], pageNum: fieldPosition.page_num, page, confidence: fieldPosition.confidence, matchMethod: fieldPosition.match_method, }; } function formatPageLabel(page?: number): string { if (!page || !Number.isFinite(page) || page <= 0) return '未定位'; return `第${page}页`; } function ExtractedFieldsPanel({ reviewPoints, onFieldClick, }: { reviewPoints: ReviewPoint[]; onFieldClick: (pointId: string | number, page?: number, value?: string, bboxHighlight?: PdfBboxHighlight) => void; }) { const handleFieldNavigate = (pointId: string, page?: number, value?: string, bboxHighlight?: PdfBboxHighlight) => { if (!page) return; const selectedText = typeof window !== 'undefined' ? window.getSelection?.()?.toString().trim() : ''; if (selectedText) return; onFieldClick(pointId, page, value, bboxHighlight); }; const fields: Array<{ key: string; displayValue: string; highlightValue?: string; isMissing: boolean; confidence?: number; page?: number; pointId: string | number; bboxHighlight?: PdfBboxHighlight; }> = []; reviewPoints.forEach((p) => { if (p.content) { Object.entries(p.content).forEach(([key, rawValue]) => { const fieldRawValue = getFieldRawValue(rawValue); const displayValue = getFieldDisplayText(fieldRawValue); const highlightValue = getFieldHighlightText(fieldRawValue); const page = getFieldPage(p, key, rawValue); const confidence = getFieldConfidence(p, key); const bboxHighlight = getFieldBboxHighlight(p, key, page); fields.push({ key, displayValue, highlightValue, isMissing: fieldRawValue == null, confidence, page, pointId: p.id, bboxHighlight, }); }); } }); return (

抽取字段 {fields.length}

置信度 · 锚定页
{fields.length === 0 ? (
暂无抽取字段
) : (
{fields.map((f, i) => (
handleFieldNavigate(f.pointId, f.page, f.highlightValue, f.bboxHighlight)} onKeyDown={(event) => { if (!f.page) return; if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); handleFieldNavigate(f.pointId, f.page, f.highlightValue, f.bboxHighlight); } }} >
{f.key}
{f.isMissing ? ( 缺失 ) : ( {f.displayValue} )}
{f.confidence == null ? '-' : `${Math.round(f.confidence * 100)}%`}
{f.page ? ( ) : (
未定位
)}
))}
)}
); } const TABS: Array<{ key: TabKey; label: string }> = [ { key: 'result', label: '评查结果' }, { key: 'fields', label: '抽取字段' }, { key: 'info', label: '文件信息' }, ]; export function DetailPanel({ activeTab, onTabChange, activeReviewPoint, reviewPoints, detailMode = 'legacy', fileInfo, reviewInfo, onReviewPointSelect, onStatusChange, onConfirmResults, onDownload, auditStatus, fileFormat, onUploadTemplate, onComparison, showComparisonButton, }: DetailPanelProps) { return ( ); }