fix: restore reviews detail layout and leaudit data wiring
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* 右栏 · 详情面板
|
||||
* 包含 3 个选项卡(评查结果、抽取字段、文件信息)+ 底部操作栏
|
||||
* 包含 3 个选项卡(评查结果、抽取字段、文件信息)和底部操作栏
|
||||
*/
|
||||
import type { ReviewPoint, CharPosition } from '../ReviewPointsList';
|
||||
import type { ReviewPoint, CharPosition, PdfBboxHighlight } from '../ReviewPointsList';
|
||||
import { ReviewPointDetailCard } from './ReviewPointDetailCard';
|
||||
import { FileInfoPanel } from './FileInfoPanel';
|
||||
|
||||
@@ -32,10 +32,11 @@ interface DetailPanelProps {
|
||||
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) => void;
|
||||
onStatusChange: (id: string | number, editAuditStatusId: string | number, status: string, message: string) => void;
|
||||
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;
|
||||
@@ -45,63 +46,191 @@ interface DetailPanelProps {
|
||||
showComparisonButton?: boolean;
|
||||
}
|
||||
|
||||
type ExtractedFieldValue = {
|
||||
value?: unknown;
|
||||
page?: number | string;
|
||||
};
|
||||
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 ExtractedFieldsPanel({
|
||||
reviewPoints,
|
||||
onFieldClick,
|
||||
}: {
|
||||
reviewPoints: ReviewPoint[];
|
||||
onFieldClick: (pointId: string | number, page: number) => void;
|
||||
onFieldClick: (pointId: string | number, page?: number, value?: string, bboxHighlight?: PdfBboxHighlight) => void;
|
||||
}) {
|
||||
const fields: Array<{ key: string; value: string; page?: number; pointName: string; pointId: string | number }> = [];
|
||||
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, data]) => {
|
||||
const fieldData = (data && typeof data === 'object' ? data : {}) as ExtractedFieldValue & { text?: string };
|
||||
const val = fieldData.value;
|
||||
const page = fieldData.page;
|
||||
const text = typeof val === 'object' && val !== null
|
||||
? ('text' in (val as Record<string, unknown>) ? String((val as Record<string, unknown>).text || '') : JSON.stringify(val))
|
||||
: String(val || '');
|
||||
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,
|
||||
value: text,
|
||||
page: page ? Number(page) : undefined,
|
||||
pointName: p.pointName,
|
||||
displayValue,
|
||||
highlightValue,
|
||||
isMissing: fieldRawValue == null,
|
||||
confidence,
|
||||
page,
|
||||
pointId: p.id,
|
||||
bboxHighlight,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-3">
|
||||
<div className="text-[11px] font-medium text-slate-400 uppercase tracking-wider mb-2">
|
||||
抽取字段 <span className="font-mono normal-case text-[10.5px]">{fields.length}</span>
|
||||
<div className="h-full flex flex-col min-h-0">
|
||||
<div className="shrink-0 px-4 py-3 border-b border-slate-100">
|
||||
<div className="flex items-baseline justify-between gap-3">
|
||||
<h3 className="text-[14px] font-semibold text-slate-900">
|
||||
抽取字段 <span className="font-mono text-[11px] text-slate-400">{fields.length}</span>
|
||||
</h3>
|
||||
<div className="text-[11px] text-slate-400">置信度 · 锚定页</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{fields.length === 0 ? (
|
||||
<div className="text-center py-6 text-[12px] text-slate-400">暂无抽取字段</div>
|
||||
<div className="text-center py-10 text-[12px] text-slate-400">暂无抽取字段</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<div className="flex-1 min-h-0 overflow-y-auto scroll-slim">
|
||||
{fields.map((f, i) => (
|
||||
<button
|
||||
<div
|
||||
key={`${f.key}-${i}`}
|
||||
type="button"
|
||||
className={`w-full border border-slate-200 rounded-md text-left hover:bg-slate-50 transition p-2.5 ${f.page ? 'cursor-pointer' : 'cursor-default opacity-70'}`}
|
||||
onClick={() => f.page && onFieldClick(f.pointId, f.page)}
|
||||
role={f.page ? 'button' : undefined}
|
||||
tabIndex={f.page ? 0 : undefined}
|
||||
className={`w-full flex items-start gap-2 px-3 py-2 border-b border-slate-100 text-left transition ${f.page ? 'cursor-pointer hover:bg-slate-50' : 'cursor-default opacity-80'}`}
|
||||
onClick={() => 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);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-[11px] text-slate-500 truncate font-medium">{f.key}</span>
|
||||
{f.page && <span className="text-[10.5px] text-slate-400 shrink-0">P{f.page}</span>}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-[12px] font-medium text-slate-800 leading-5 break-words">{f.key}</div>
|
||||
<div className="mt-0.5 select-text cursor-text">
|
||||
{f.isMissing ? (
|
||||
<span className="text-[11px] text-red-500">缺失</span>
|
||||
) : (
|
||||
<span className="text-[11px] text-slate-500 leading-5 whitespace-pre-wrap break-words">{f.displayValue}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{f.value && <div className="text-[12px] text-slate-700 mt-1 leading-relaxed line-clamp-2">{f.value}</div>}
|
||||
<div className="text-[10px] text-slate-400 mt-0.5">{f.pointName}</div>
|
||||
</button>
|
||||
|
||||
<div className="shrink-0 text-right min-w-[56px] pt-0.5">
|
||||
<div className={`font-mono text-[10.5px] ${f.confidence == null ? 'text-slate-400' : f.confidence < 0.8 ? 'text-orange-600' : 'text-slate-500'}`}>
|
||||
{f.confidence == null ? '-' : `${Math.round(f.confidence * 100)}%`}
|
||||
</div>
|
||||
{f.page ? (
|
||||
<button
|
||||
type="button"
|
||||
className="mt-0.5 text-[10px] text-[#00684a] hover:underline"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleFieldNavigate(f.pointId, f.page, f.highlightValue, f.bboxHighlight);
|
||||
}}
|
||||
>
|
||||
p.{f.page}
|
||||
</button>
|
||||
) : (
|
||||
<div className="mt-0.5 text-[10px] text-slate-300">-</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -120,6 +249,7 @@ export function DetailPanel({
|
||||
onTabChange,
|
||||
activeReviewPoint,
|
||||
reviewPoints,
|
||||
detailMode = 'legacy',
|
||||
fileInfo,
|
||||
reviewInfo,
|
||||
onReviewPointSelect,
|
||||
@@ -186,6 +316,7 @@ export function DetailPanel({
|
||||
onReviewPointSelect={onReviewPointSelect}
|
||||
onStatusChange={onStatusChange}
|
||||
fileFormat={fileFormat}
|
||||
detailMode={detailMode}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
@@ -199,8 +330,8 @@ export function DetailPanel({
|
||||
{activeTab === 'fields' && (
|
||||
<ExtractedFieldsPanel
|
||||
reviewPoints={reviewPoints}
|
||||
onFieldClick={(pointId, page) => {
|
||||
onReviewPointSelect(pointId, page);
|
||||
onFieldClick={(pointId, page, value, bboxHighlight) => {
|
||||
onReviewPointSelect(pointId, page, undefined, value, bboxHighlight);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user