Files
leaudit-platform-frontend/app/components/reviews/previewComponents/PdfPreviewTest.tsx
T

733 lines
28 KiB
TypeScript

/**
* PDF 预览组件(设计稿 7c-简化C-极简 · 中栏实现)
*
* 与 PdfPreview.tsx 对齐的 props 签名,可直接替换用于测试。
* 变化:
* - 单页视口(非连续滚动)
* - 顶部工具栏:略缩图开关、上/下页、页码、缩放、当前高亮、全屏
* - 左侧 132px 略缩图面板(filtered / all 两种模式),真实 react-pdf 渲染
* - 状态徽标由 reviewPoints 聚合得到
*/
import { useState, useEffect, useMemo, useRef, useCallback } from 'react';
import { Document, Page, pdfjs } from 'react-pdf';
import { toastService } from '~/components/ui/Toast';
import type { ReviewPoint, PdfBboxHighlight } from '../ReviewPointsList';
import 'react-pdf/dist/esm/Page/TextLayer.css';
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.js';
// ============================================================
// 类型
// ============================================================
type WorstStatus = 'fail' | 'warn' | 'pending' | 'pass';
interface PageAgg {
worst: WorstStatus;
count: number;
issues: number;
}
interface PdfPreviewProps {
filePath: string;
targetPage?: number;
charPositions?: Array<{ box: number[][]; char: string; score: number }>;
bboxHighlight?: PdfBboxHighlight;
isStructuredView?: boolean;
activeReviewPointResultId?: string | null;
pageOffset?: number;
onNumPagesChange?: (numPages: number) => void;
onZoomChange?: (zoomLevel: number) => void;
// 新增(可选):用于派生略缩图徽标和当前高亮标签
reviewPoints?: ReviewPoint[];
}
const THUMB_WIDTH = 112;
const THUMB_ESTIMATED_HEIGHT = 210;
const THUMB_OVERSCAN = 3;
const MAIN_PAGE_MAX_DEVICE_PIXEL_RATIO = 1.5;
// ============================================================
// ReviewPoint → 状态映射
// ============================================================
const STATUS_ORDER: Record<WorstStatus, number> = { fail: 0, warn: 1, pending: 2, pass: 3 };
function getContentItemPage(item: ReviewPoint['content'][string]): number | string | undefined {
return typeof item === 'string' ? undefined : item?.page;
}
const STATUS_BADGE: Record<WorstStatus, { cls: string; ic: string }> = {
fail: { cls: 'bg-red-500', ic: 'ri-close-circle-fill' },
warn: { cls: 'bg-amber-500', ic: 'ri-lightbulb-flash-fill' },
pending: { cls: 'bg-orange-500', ic: 'ri-question-fill' },
pass: { cls: 'bg-emerald-500', ic: 'ri-checkbox-circle-fill' },
};
function classifyReviewPoint(p: ReviewPoint): WorstStatus | 'skipped' {
const status = p.status;
if (status === 'notApplicable' || status === 'not_applicable') return 'skipped';
if (p.result === true || (p.result === undefined && status === 'success')) return 'pass';
if (p.result === false) {
if (status === 'error') return 'fail';
if (status === 'warning' || status === 'info') return 'warn';
}
if (status === 'success') return 'pass';
if (status === 'warning' || status === 'info') return 'warn';
if (status === 'error') return 'fail';
return 'pass';
}
/** 把 ReviewPoint 涉及的所有页号(字段级)抽出 */
function getPointPages(p: ReviewPoint): number[] {
const set = new Set<number>();
const addMaybe = (v: unknown) => {
const n = typeof v === 'string' ? parseInt(v, 10) : typeof v === 'number' ? v : NaN;
if (Number.isFinite(n) && n > 0) set.add(n);
};
if (p.contentPage) Object.values(p.contentPage).forEach(addMaybe);
if (p.content) Object.values(p.content).forEach(v => addMaybe(getContentItemPage(v)));
return [...set].sort((a, b) => a - b);
}
/** 当前规则在某页涉及的字段名列表 */
function getFieldsOnPage(p: ReviewPoint, page: number): string[] {
const fields: string[] = [];
const matches = (v: unknown) => {
const n = typeof v === 'string' ? parseInt(v, 10) : typeof v === 'number' ? v : NaN;
return n === page;
};
if (p.contentPage) {
Object.entries(p.contentPage).forEach(([k, v]) => {
if (matches(v)) fields.push(k);
});
}
if (!fields.length && p.content) {
Object.entries(p.content).forEach(([k, v]) => {
if (matches(getContentItemPage(v))) fields.push(k);
});
}
return fields;
}
function clamp(value: number, min: number, max: number): number {
return Math.min(max, Math.max(min, value));
}
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));
}
// ============================================================
// 组件
// ============================================================
export function PdfPreviewTest({
filePath,
targetPage,
charPositions,
bboxHighlight,
isStructuredView = false,
activeReviewPointResultId,
pageOffset = 0,
onNumPagesChange,
onZoomChange,
reviewPoints,
}: PdfPreviewProps) {
const [numPages, setNumPages] = useState<number | null>(null);
const [currentPage, setCurrentPage] = useState<number>(1);
const [zoomLevel, setZoomLevel] = useState(100);
const [loadError, setLoadError] = useState<string | null>(null);
const [showThumbs, setShowThumbs] = useState(true);
const [thumbMode, setThumbMode] = useState<'filtered' | 'all'>('filtered');
const [coordinateScale, setCoordinateScale] = useState(0.83);
const [isScaleAutoCalculated, setIsScaleAutoCalculated] = useState(false);
const [pageInputValue, setPageInputValue] = useState<string>('');
const [pageRenderTick, setPageRenderTick] = useState(0);
const [thumbsScrollTop, setThumbsScrollTop] = useState(0);
const [thumbsViewportHeight, setThumbsViewportHeight] = useState(0);
const viewportRef = useRef<HTMLDivElement>(null);
const thumbsPanelRef = useRef<HTMLDivElement>(null);
const fileUrl = useMemo(() => `/api/pdf-proxy?path=${encodeURIComponent(filePath)}`, [filePath]);
const activePoint = useMemo<ReviewPoint | undefined>(
() => reviewPoints?.find(p => p.id === activeReviewPointResultId),
[reviewPoints, activeReviewPointResultId],
);
const rulePages = useMemo<number[]>(() => {
if (!activePoint) return [];
return getPointPages(activePoint).map(page => page + pageOffset);
}, [activePoint, pageOffset]);
const pageStatusMap = useMemo<Map<number, PageAgg>>(() => {
const map = new Map<number, PageAgg>();
if (!reviewPoints?.length) return map;
reviewPoints.forEach(point => {
const status = classifyReviewPoint(point);
if (status === 'skipped') return;
getPointPages(point).map(page => page + pageOffset).forEach(page => {
const current = map.get(page) || { worst: 'pass', count: 0, issues: 0 };
if (STATUS_ORDER[status] < STATUS_ORDER[current.worst]) current.worst = status;
current.count += 1;
if (status !== 'pass') current.issues += 1;
map.set(page, current);
});
});
return map;
}, [reviewPoints, pageOffset]);
const highlightLabel = useMemo(() => {
if (!activePoint) return null;
const code = activePoint.pointCode || activePoint.id;
const name = activePoint.pointName || activePoint.title || '';
return `${code}${name ? ' · ' + name : ''}`;
}, [activePoint]);
const effThumbMode: 'filtered' | 'all' =
thumbMode === 'filtered' && rulePages.length > 0 ? 'filtered' : 'all';
const thumbPages = useMemo<number[]>(() => {
if (!numPages) return [];
if (effThumbMode === 'filtered') return rulePages.filter(page => page >= 1 && page <= numPages);
return Array.from({ length: numPages }, (_, index) => index + 1);
}, [effThumbMode, rulePages, numPages]);
const rulePageSet = useMemo(() => new Set(rulePages), [rulePages]);
const totalThumbHeight = thumbPages.length * THUMB_ESTIMATED_HEIGHT;
const visibleThumbRange = useMemo(() => {
if (!thumbPages.length) {
return { start: 0, end: 0 };
}
const viewportHeight = thumbsViewportHeight || THUMB_ESTIMATED_HEIGHT * 3;
const start = Math.max(0, Math.floor(thumbsScrollTop / THUMB_ESTIMATED_HEIGHT) - THUMB_OVERSCAN);
const visibleCount = Math.ceil(viewportHeight / THUMB_ESTIMATED_HEIGHT) + THUMB_OVERSCAN * 2;
const end = Math.min(thumbPages.length, start + visibleCount);
return { start, end };
}, [thumbPages.length, thumbsScrollTop, thumbsViewportHeight]);
const visibleThumbItems = useMemo(
() => thumbPages.slice(visibleThumbRange.start, visibleThumbRange.end),
[thumbPages, visibleThumbRange.start, visibleThumbRange.end],
);
const mainPageDevicePixelRatio = useMemo(() => {
if (typeof window === 'undefined') return 1;
return Math.min(window.devicePixelRatio || 1, MAIN_PAGE_MAX_DEVICE_PIXEL_RATIO);
}, []);
useEffect(() => {
if (numPages && onNumPagesChange) onNumPagesChange(numPages);
}, [numPages, onNumPagesChange]);
useEffect(() => {
if (onZoomChange) onZoomChange(zoomLevel);
}, [zoomLevel, onZoomChange]);
useEffect(() => {
if (targetPage && numPages) {
const next = Math.max(1, Math.min(numPages, targetPage + pageOffset));
setCurrentPage(next);
}
}, [targetPage, numPages, pageOffset, activeReviewPointResultId]);
useEffect(() => {
if (activeReviewPointResultId) setThumbMode('filtered');
}, [activeReviewPointResultId]);
useEffect(() => {
setIsScaleAutoCalculated(false);
}, [filePath]);
useEffect(() => {
const host = thumbsPanelRef.current;
if (!host) return;
const currentIndex = thumbPages.indexOf(currentPage);
if (currentIndex < 0) return;
const itemTop = currentIndex * THUMB_ESTIMATED_HEIGHT;
const itemBottom = itemTop + THUMB_ESTIMATED_HEIGHT;
const viewportTop = host.scrollTop;
const viewportBottom = viewportTop + host.clientHeight;
if (itemTop < viewportTop) {
host.scrollTo({ top: itemTop, behavior: 'smooth' });
return;
}
if (itemBottom > viewportBottom) {
host.scrollTo({
top: Math.max(0, itemBottom - host.clientHeight),
behavior: 'smooth',
});
}
}, [currentPage, thumbMode, showThumbs, thumbPages]);
useEffect(() => {
const host = thumbsPanelRef.current;
if (!host) return;
const updateViewport = () => {
setThumbsViewportHeight(host.clientHeight);
setThumbsScrollTop(host.scrollTop);
};
updateViewport();
const handleScroll = () => {
setThumbsScrollTop(host.scrollTop);
};
host.addEventListener('scroll', handleScroll, { passive: true });
let resizeObserver: ResizeObserver | null = null;
if (typeof ResizeObserver !== 'undefined') {
resizeObserver = new ResizeObserver(() => {
updateViewport();
});
resizeObserver.observe(host);
}
return () => {
host.removeEventListener('scroll', handleScroll);
resizeObserver?.disconnect();
};
}, [showThumbs, thumbMode, filePath]);
const onDocumentLoadSuccess = useCallback(
({ numPages: loadedNumPages }: { numPages: number }) => {
setNumPages(loadedNumPages);
if (targetPage) {
setCurrentPage(Math.max(1, Math.min(loadedNumPages, targetPage + pageOffset)));
} else {
setCurrentPage(page => Math.max(1, Math.min(loadedNumPages, page)));
}
},
[targetPage, pageOffset],
);
const onDocumentLoadError = useCallback((error: Error) => {
console.error('PDF加载错误:', error);
setLoadError('PDF文档加载失败:' + (error.message || '未知错误'));
}, []);
const onMainPageLoadSuccess = useCallback(
(page: any) => {
setPageRenderTick(tick => tick + 1);
if (isScaleAutoCalculated) return;
setTimeout(() => {
const pdfOriginalWidthPt = page.view?.[2] || page.originalWidth || page.width;
const canvas = viewportRef.current?.querySelector('.pdf-main-canvas .react-pdf__Page__canvas') as HTMLCanvasElement | null;
if (canvas && pdfOriginalWidthPt) {
const canvasDisplayWidth = canvas.offsetWidth;
const currentScale = zoomLevel / 100;
const autoScale = canvasDisplayWidth / currentScale / pdfOriginalWidthPt;
setCoordinateScale(autoScale);
setIsScaleAutoCalculated(true);
}
}, 200);
},
[isScaleAutoCalculated, zoomLevel],
);
const goPrev = () => setCurrentPage(page => Math.max(1, page - 1));
const goNext = () => setCurrentPage(page => Math.min(numPages || page, page + 1));
const zoomIn = () => setZoomLevel(level => Math.min(200, level + 10));
const zoomOut = () => setZoomLevel(level => Math.max(50, level - 10));
const jumpToHighlight = () => {
if (!activePoint || rulePages.length === 0) {
toastService.info('当前规则无关联页');
return;
}
const firstPage = rulePages[0];
if (numPages && firstPage >= 1 && firstPage <= numPages) setCurrentPage(firstPage);
};
const handlePageInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setPageInputValue(e.target.value.replace(/\D/g, ''));
};
const handlePageJump = () => {
if (!pageInputValue || !numPages) return;
const nextPage = parseInt(pageInputValue, 10);
if (nextPage > 0 && nextPage <= numPages) {
setCurrentPage(nextPage);
setPageInputValue('');
return;
}
toastService.warning(`请输入有效页码 (1-${numPages})`);
setPageInputValue('');
};
const bboxRectHighlight = useMemo(() => {
if (!bboxHighlight || !isValidQuad(bboxHighlight.bbox) || !isValidQuad(bboxHighlight.pageBox)) return null;
const expectedPage = bboxHighlight.page ?? (typeof bboxHighlight.pageNum === 'number' ? bboxHighlight.pageNum + 1 : targetPage);
if (!expectedPage || currentPage !== expectedPage + pageOffset) return null;
const canvas = viewportRef.current?.querySelector('.pdf-main-canvas .react-pdf__Page__canvas') as HTMLCanvasElement | null;
if (!canvas) return null;
const [pageX0, pageY0, pageX1, pageY1] = bboxHighlight.pageBox;
const [bboxLeft, bboxTop, bboxRight, bboxBottom] = bboxHighlight.bbox;
const pageWidth = pageX1 - pageX0;
const pageHeight = pageY1 - pageY0;
if (pageWidth <= 0 || pageHeight <= 0) return null;
const left = clamp(Math.min(bboxLeft, bboxRight), pageX0, pageX1);
const right = clamp(Math.max(bboxLeft, bboxRight), pageX0, pageX1);
const top = clamp(Math.min(bboxTop, bboxBottom), pageY0, pageY1);
const bottom = clamp(Math.max(bboxTop, bboxBottom), pageY0, pageY1);
if (right <= left || bottom <= top) return null;
return {
x: ((left - pageX0) / pageWidth) * canvas.offsetWidth,
y: ((top - pageY0) / pageHeight) * canvas.offsetHeight,
width: ((right - left) / pageWidth) * canvas.offsetWidth,
height: ((bottom - top) / pageHeight) * canvas.offsetHeight,
text: bboxHighlight.fieldKey,
};
}, [bboxHighlight, targetPage, currentPage, pageOffset, zoomLevel, pageRenderTick]);
const charRectHighlight = useMemo(() => {
if (bboxRectHighlight || !charPositions?.length || !targetPage) return null;
if (currentPage !== targetPage + pageOffset) return null;
const scale = zoomLevel / 100;
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
const chars: string[] = [];
charPositions.forEach(position => {
chars.push(position.char);
position.box.forEach(([x, y]) => {
if (x < minX) minX = x;
if (x > maxX) maxX = x;
if (y < minY) minY = y;
if (y > maxY) maxY = y;
});
});
return {
x: minX * coordinateScale * scale,
y: minY * coordinateScale * scale,
width: (maxX - minX) * coordinateScale * scale,
height: (maxY - minY) * coordinateScale * scale,
text: chars.join(''),
};
}, [bboxRectHighlight, charPositions, targetPage, currentPage, pageOffset, zoomLevel, coordinateScale, pageRenderTick]);
const mainHighlight = bboxRectHighlight || charRectHighlight;
if (loadError) {
return (
<div className="w-full h-full grid place-items-center text-red-500 p-4">{loadError}</div>
);
}
return (
<Document
file={fileUrl}
onLoadSuccess={onDocumentLoadSuccess}
onLoadError={onDocumentLoadError}
className="flex flex-col min-h-0 w-full h-full"
loading={<div className="flex-1 grid place-items-center text-slate-400 text-sm">PDF </div>}
error={<div className="flex-1 grid place-items-center text-red-500">PDF </div>}
noData={<div className="flex-1 grid place-items-center text-slate-400"></div>}
>
<section
className="flex flex-col flex-1 min-h-0 w-full bg-slate-100 border border-slate-200 rounded"
>
{/* ═════ 顶部工具栏 ═════ */}
<div className="shrink-0 h-11 px-4 flex items-center justify-between bg-white border-b border-slate-200 text-[12.5px] text-slate-600">
<div className="flex items-center gap-2">
<button
onClick={() => setShowThumbs(s => !s)}
className={`w-5 h-7 grid place-items-center rounded hover:bg-slate-100 ${
showThumbs ? 'text-primary' : 'text-slate-400'
}`}
title="显示/隐藏页面缩略图"
>
<i className="ri-layout-masonry-line"></i>
</button>
<span className="mx-0 text-slate-300">|</span>
<button
onClick={goPrev}
disabled={currentPage <= 1}
className="w-5 h-7 grid place-items-center rounded hover:bg-slate-100 disabled:opacity-40 disabled:cursor-not-allowed"
title="上一页"
>
<i className="ri-arrow-left-s-line"></i>
</button>
<span className="font-mono tabular-nums">
<input
type="text"
value={pageInputValue !== '' ? pageInputValue : currentPage}
onChange={handlePageInputChange}
onFocus={e => e.currentTarget.select()}
onBlur={handlePageJump}
onKeyDown={e => {
if (e.key === 'Enter') handlePageJump();
}}
className="w-6 h-6 text-center bg-slate-50 border border-slate-200 rounded text-[12px] font-mono outline-none focus:border-primary"
/>
<span className="text-slate-400"> / {numPages ?? '-'}</span>
</span>
<button
onClick={goNext}
disabled={!numPages || currentPage >= numPages}
className="w-5 h-7 grid place-items-center rounded hover:bg-slate-100 disabled:opacity-40 disabled:cursor-not-allowed"
title="下一页"
>
<i className="ri-arrow-right-s-line"></i>
</button>
<span className="mx-0 text-slate-300">|</span>
<button
onClick={zoomOut}
disabled={zoomLevel <= 50}
className="w-5 h-7 grid place-items-center rounded hover:bg-slate-100 disabled:opacity-40"
title="缩小"
>
<i className="ri-zoom-out-line"></i>
</button>
<span className="font-mono min-w-[42px] text-center">{zoomLevel}%</span>
<button
onClick={zoomIn}
disabled={zoomLevel >= 200}
className="w-5 h-7 grid place-items-center rounded hover:bg-slate-100 disabled:opacity-40"
title="放大"
>
<i className="ri-zoom-in-line"></i>
</button>
</div>
<div className="flex items-center gap-2 text-[11.5px] min-w-0">
{highlightLabel && (
<>
<span className="text-slate-400 shrink-0">:</span>
<span
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded bg-amber-50 text-amber-800 border border-amber-200 max-w-[220px]"
title={highlightLabel}
>
<i className="ri-focus-3-line shrink-0"></i>
<span className="truncate">{highlightLabel}</span>
</span>
<button
onClick={jumpToHighlight}
className="w-7 h-7 grid place-items-center rounded hover:bg-slate-100 shrink-0"
title="跳转到高亮页"
>
<i className="ri-crosshair-line"></i>
</button>
</>
)}
</div>
</div>
{/* ═════ 主体:略缩图 + 视口 ═════ */}
<div className="flex-1 min-h-0 flex min-w-0">
{/* ── 略缩图面板 ── */}
{showThumbs && (
<div className="w-[132px] shrink-0 bg-white border-r border-slate-200 flex flex-col min-h-0">
{/* 模式切换条 */}
<div className="shrink-0 px-2 py-2 border-b border-slate-100">
<div className="inline-flex rounded border border-slate-200 bg-slate-50 p-0.5 text-[10.5px] w-full">
<button
onClick={() => setThumbMode('filtered')}
disabled={rulePages.length === 0}
className={`flex-1 h-5 rounded font-medium ${
effThumbMode === 'filtered'
? 'bg-white shadow-sm text-primary'
: 'text-slate-500 hover:text-slate-700'
} ${rulePages.length === 0 ? 'opacity-40 cursor-not-allowed' : ''}`}
>
</button>
<button
onClick={() => setThumbMode('all')}
className={`flex-1 h-5 rounded font-medium ${
effThumbMode === 'all'
? 'bg-white shadow-sm text-primary'
: 'text-slate-500 hover:text-slate-700'
}`}
>
{numPages ?? ''}
</button>
</div>
</div>
{/* 略缩图列表 */}
<div
ref={thumbsPanelRef}
className="flex-1 overflow-y-auto py-2 px-2"
>
{numPages === null ? (
<div className="text-center text-[11px] text-slate-400 py-4"></div>
) : thumbPages.length === 0 ? (
<div className="text-center text-[11px] text-slate-400 py-8">
<i className="ri-forbid-2-line text-2xl text-slate-300"></i>
<div className="mt-1"></div>
</div>
) : (
<div className="relative w-full" style={{ height: totalThumbHeight }}>
{visibleThumbItems.map((p, visibleIndex) => {
const itemIndex = visibleThumbRange.start + visibleIndex;
const info = pageStatusMap.get(p);
const isCur = p === currentPage;
const isRulePage = rulePageSet.has(p);
let badge: React.ReactNode = null;
if (info) {
const b = STATUS_BADGE[info.worst];
const num = info.issues > 0 ? info.issues : info.worst === 'pass' ? '' : info.count;
badge = (
<span
className={`absolute top-1 right-1 inline-flex items-center justify-center min-w-[14px] h-[14px] px-0.5 rounded-full text-[9px] font-semibold text-white ${b.cls} shadow ring-1 ring-white`}
>
{num ? num : <i className={`${b.ic} text-[9px]`}></i>}
</span>
);
}
const frameCls = isCur
? 'ring-2 ring-[#00684a] shadow-md'
: effThumbMode === 'all' && isRulePage
? 'ring-1 ring-[#00684a]/40 shadow-sm'
: 'border border-slate-200 group-hover:border-slate-400 shadow-sm';
let fieldsLabel: React.ReactNode = null;
if (effThumbMode === 'filtered' && activePoint) {
const fs = getFieldsOnPage(activePoint, p - pageOffset);
const txt = fs.length ? fs.join(' · ') : '规则锚定页';
fieldsLabel = (
<div
className="text-[10px] leading-tight text-slate-500 text-center mt-0.5 line-clamp-2"
title={txt}
>
{txt}
</div>
);
}
return (
<button
key={p}
data-thumb-page={p}
onClick={() => setCurrentPage(p)}
className="absolute left-0 block w-full group"
style={{ top: itemIndex * THUMB_ESTIMATED_HEIGHT }}
title={`${p}`}
>
<div className={`relative rounded overflow-hidden bg-white transition ${frameCls}`}>
<div className="w-full bg-gradient-to-b from-white to-slate-50 overflow-hidden">
<Page
pageNumber={p}
width={THUMB_WIDTH}
devicePixelRatio={1}
renderTextLayer={false}
renderAnnotationLayer={false}
loading={<div className="w-full h-[150px] bg-slate-50" />}
error={<div className="w-full h-[150px] bg-slate-50" />}
/>
</div>
{badge}
</div>
<div
className={`text-center text-[10.5px] mt-1 ${
isCur ? 'text-[#00684a] font-semibold' : 'text-slate-500 group-hover:text-slate-700'
}`}
>
{p}
</div>
{fieldsLabel}
</button>
);
})}
</div>
)}
</div>
</div>
)}
{/* ── 视口(单页) ── */}
<div
ref={viewportRef}
className="flex-1 min-h-0 min-w-0 overflow-auto p-4"
style={{ display: 'flex', justifyContent: 'center', alignItems: 'flex-start' }}
>
<div
className="pdf-main-canvas"
style={{
position: 'relative',
display: 'inline-block',
textAlign: 'left',
flexShrink: 0,
}}
>
{numPages !== null && (
<>
<Page
pageNumber={Math.min(currentPage, numPages)}
scale={zoomLevel / 100}
devicePixelRatio={mainPageDevicePixelRatio}
renderTextLayer={false}
renderAnnotationLayer={false}
onLoadSuccess={onMainPageLoadSuccess}
className="border border-slate-200 shadow-lg bg-white"
loading={<div className="w-[600px] h-[800px] grid place-items-center text-slate-400"></div>}
/>
{mainHighlight && (
<svg
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
pointerEvents: 'none',
zIndex: 12,
}}
>
<rect
x={mainHighlight.x}
y={mainHighlight.y}
width={mainHighlight.width}
height={mainHighlight.height}
// fill="#00AA00"
fill="#abf694"
fillOpacity="0.1"
stroke="#00684a"
strokeWidth="0.5"
rx="2"
>
<title>{`高亮文本: ${mainHighlight.text}`}</title>
</rect>
</svg>
)}
</>
)}
</div>
</div>
</div>
</section>
</Document>
);
}