fix: restore reviews detail layout and leaudit data wiring
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
import { useState, useEffect, useMemo, useRef, useCallback } from 'react';
|
||||
import { Document, Page, pdfjs } from 'react-pdf';
|
||||
import { toastService } from '~/components/ui/Toast';
|
||||
import type { ReviewPoint } from '../ReviewPointsList';
|
||||
import type { ReviewPoint, PdfBboxHighlight } from '../ReviewPointsList';
|
||||
|
||||
import 'react-pdf/dist/esm/Page/TextLayer.css';
|
||||
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
|
||||
@@ -33,6 +33,7 @@ interface PdfPreviewProps {
|
||||
filePath: string;
|
||||
targetPage?: number;
|
||||
charPositions?: Array<{ box: number[][]; char: string; score: number }>;
|
||||
bboxHighlight?: PdfBboxHighlight;
|
||||
isStructuredView?: boolean;
|
||||
activeReviewPointResultId?: string | null;
|
||||
pageOffset?: number;
|
||||
@@ -43,11 +44,20 @@ interface PdfPreviewProps {
|
||||
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' },
|
||||
@@ -77,7 +87,7 @@ function getPointPages(p: ReviewPoint): number[] {
|
||||
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(v?.page));
|
||||
if (p.content) Object.values(p.content).forEach(v => addMaybe(getContentItemPage(v)));
|
||||
return [...set].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
@@ -95,12 +105,20 @@ function getFieldsOnPage(p: ReviewPoint, page: number): string[] {
|
||||
}
|
||||
if (!fields.length && p.content) {
|
||||
Object.entries(p.content).forEach(([k, v]) => {
|
||||
if (matches(v?.page)) fields.push(k);
|
||||
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));
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 组件
|
||||
// ============================================================
|
||||
@@ -108,6 +126,7 @@ export function PdfPreviewTest({
|
||||
filePath,
|
||||
targetPage,
|
||||
charPositions,
|
||||
bboxHighlight,
|
||||
isStructuredView = false,
|
||||
activeReviewPointResultId,
|
||||
pageOffset = 0,
|
||||
@@ -115,63 +134,55 @@ export function PdfPreviewTest({
|
||||
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 fileUrl = useMemo(() => `/api/pdf-proxy?path=${encodeURIComponent(filePath)}`, [filePath]);
|
||||
|
||||
const activePoint = useMemo<ReviewPoint | undefined>(
|
||||
() => reviewPoints?.find(p => p.id === activeReviewPointResultId),
|
||||
[reviewPoints, activeReviewPointResultId],
|
||||
);
|
||||
|
||||
// 当前规则涉及的页(字段级,带 pageOffset)
|
||||
const rulePages = useMemo<number[]>(() => {
|
||||
if (!activePoint) return [];
|
||||
return getPointPages(activePoint).map(p => p + pageOffset);
|
||||
return getPointPages(activePoint).map(page => page + pageOffset);
|
||||
}, [activePoint, pageOffset]);
|
||||
|
||||
// 每页状态聚合
|
||||
const pageStatusMap = useMemo<Map<number, PageAgg>>(() => {
|
||||
const m = new Map<number, PageAgg>();
|
||||
if (!reviewPoints?.length) return m;
|
||||
reviewPoints.forEach(p => {
|
||||
const cls = classifyReviewPoint(p);
|
||||
if (cls === 'skipped') return;
|
||||
const pages = getPointPages(p).map(x => x + pageOffset);
|
||||
pages.forEach(pg => {
|
||||
const cur = m.get(pg) || { worst: 'pass', count: 0, issues: 0 };
|
||||
if (STATUS_ORDER[cls] < STATUS_ORDER[cur.worst]) cur.worst = cls;
|
||||
cur.count += 1;
|
||||
if (cls !== 'pass') cur.issues += 1;
|
||||
m.set(pg, cur);
|
||||
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 m;
|
||||
|
||||
return map;
|
||||
}, [reviewPoints, pageOffset]);
|
||||
|
||||
// 当前高亮标签(工具栏右侧)
|
||||
const highlightLabel = useMemo(() => {
|
||||
if (!activePoint) return null;
|
||||
const code = activePoint.pointCode || activePoint.id;
|
||||
@@ -179,7 +190,42 @@ export function PdfPreviewTest({
|
||||
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]);
|
||||
@@ -188,7 +234,6 @@ export function PdfPreviewTest({
|
||||
if (onZoomChange) onZoomChange(zoomLevel);
|
||||
}, [zoomLevel, onZoomChange]);
|
||||
|
||||
// ---------- targetPage 跳转 ----------
|
||||
useEffect(() => {
|
||||
if (targetPage && numPages) {
|
||||
const next = Math.max(1, Math.min(numPages, targetPage + pageOffset));
|
||||
@@ -196,33 +241,77 @@ export function PdfPreviewTest({
|
||||
}
|
||||
}, [targetPage, numPages, pageOffset, activeReviewPointResultId]);
|
||||
|
||||
// ---------- 切换规则:重置略缩图模式为 filtered ----------
|
||||
useEffect(() => {
|
||||
if (activeReviewPointResultId) setThumbMode('filtered');
|
||||
}, [activeReviewPointResultId]);
|
||||
|
||||
// ---------- 文件路径变化:重置坐标自动计算 ----------
|
||||
useEffect(() => {
|
||||
setIsScaleAutoCalculated(false);
|
||||
}, [filePath]);
|
||||
|
||||
// ---------- 略缩图滚动到当前页 ----------
|
||||
useEffect(() => {
|
||||
const host = thumbsPanelRef.current;
|
||||
if (!host) return;
|
||||
const el = host.querySelector<HTMLElement>(`[data-thumb-page="${currentPage}"]`);
|
||||
if (el) el.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
||||
}, [currentPage, thumbMode, showThumbs]);
|
||||
|
||||
// ---------- PDF 加载 ----------
|
||||
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: n }: { numPages: number }) => {
|
||||
setNumPages(n);
|
||||
// 初始页:优先 targetPage,否则第 1 页
|
||||
({ numPages: loadedNumPages }: { numPages: number }) => {
|
||||
setNumPages(loadedNumPages);
|
||||
if (targetPage) {
|
||||
setCurrentPage(Math.max(1, Math.min(n, targetPage + pageOffset)));
|
||||
setCurrentPage(Math.max(1, Math.min(loadedNumPages, targetPage + pageOffset)));
|
||||
} else {
|
||||
setCurrentPage(p => Math.max(1, Math.min(n, p)));
|
||||
setCurrentPage(page => Math.max(1, Math.min(loadedNumPages, page)));
|
||||
}
|
||||
},
|
||||
[targetPage, pageOffset],
|
||||
@@ -233,19 +322,18 @@ export function PdfPreviewTest({
|
||||
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;
|
||||
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;
|
||||
const autoScale = canvasDisplayWidth / currentScale / pdfOriginalWidthPt;
|
||||
setCoordinateScale(autoScale);
|
||||
setIsScaleAutoCalculated(true);
|
||||
}
|
||||
@@ -254,71 +342,89 @@ export function PdfPreviewTest({
|
||||
[isScaleAutoCalculated, zoomLevel],
|
||||
);
|
||||
|
||||
// ---------- 翻页 / 缩放 ----------
|
||||
const goPrev = () => setCurrentPage(p => Math.max(1, p - 1));
|
||||
const goNext = () => setCurrentPage(p => Math.min(numPages || p, p + 1));
|
||||
const zoomIn = () => setZoomLevel(z => Math.min(200, z + 10));
|
||||
const zoomOut = () => setZoomLevel(z => Math.max(50, z - 10));
|
||||
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 first = rulePages[0];
|
||||
if (numPages && first >= 1 && first <= numPages) setCurrentPage(first);
|
||||
|
||||
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 n = parseInt(pageInputValue, 10);
|
||||
if (n > 0 && n <= numPages) {
|
||||
setCurrentPage(n);
|
||||
setPageInputValue('');
|
||||
} else {
|
||||
toastService.warning(`请输入有效页码 (1-${numPages})`);
|
||||
const nextPage = parseInt(pageInputValue, 10);
|
||||
if (nextPage > 0 && nextPage <= numPages) {
|
||||
setCurrentPage(nextPage);
|
||||
setPageInputValue('');
|
||||
return;
|
||||
}
|
||||
|
||||
toastService.warning(`请输入有效页码 (1-${numPages})`);
|
||||
setPageInputValue('');
|
||||
};
|
||||
|
||||
// ---------- 高亮矩形(对齐原 PdfPreview 的字符位置) ----------
|
||||
const mainHighlight = useMemo(() => {
|
||||
if (!charPositions?.length || !targetPage) {
|
||||
console.log('[PdfPreviewTest] highlight skipped: no charPositions/targetPage', {
|
||||
hasCharPositions: !!charPositions?.length,
|
||||
targetPage,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
if (currentPage !== targetPage + pageOffset) {
|
||||
console.log('[PdfPreviewTest] highlight skipped: page mismatch', {
|
||||
currentPage,
|
||||
targetPage,
|
||||
pageOffset,
|
||||
expected: targetPage + pageOffset,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
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,
|
||||
minY = Infinity,
|
||||
maxX = -Infinity,
|
||||
maxY = -Infinity;
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
const chars: string[] = [];
|
||||
charPositions.forEach(cp => {
|
||||
chars.push(cp.char);
|
||||
cp.box.forEach(pt => {
|
||||
const [x, y] = pt;
|
||||
|
||||
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,
|
||||
@@ -326,21 +432,10 @@ export function PdfPreviewTest({
|
||||
height: (maxY - minY) * coordinateScale * scale,
|
||||
text: chars.join(''),
|
||||
};
|
||||
}, [charPositions, targetPage, currentPage, pageOffset, zoomLevel, coordinateScale]);
|
||||
}, [bboxRectHighlight, charPositions, targetPage, currentPage, pageOffset, zoomLevel, coordinateScale, pageRenderTick]);
|
||||
|
||||
// ---------- 略缩图可见页列表 ----------
|
||||
const effThumbMode: 'filtered' | 'all' =
|
||||
thumbMode === 'filtered' && rulePages.length > 0 ? 'filtered' : 'all';
|
||||
const mainHighlight = bboxRectHighlight || charRectHighlight;
|
||||
|
||||
const thumbPages = useMemo<number[]>(() => {
|
||||
if (!numPages) return [];
|
||||
if (effThumbMode === 'filtered') return rulePages.filter(p => p >= 1 && p <= numPages);
|
||||
return Array.from({ length: numPages }, (_, i) => i + 1);
|
||||
}, [effThumbMode, rulePages, numPages]);
|
||||
|
||||
// ============================================================
|
||||
// 渲染
|
||||
// ============================================================
|
||||
if (loadError) {
|
||||
return (
|
||||
<div className="w-full h-full grid place-items-center text-red-500 p-4">{loadError}</div>
|
||||
@@ -391,7 +486,7 @@ export function PdfPreviewTest({
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter') handlePageJump();
|
||||
}}
|
||||
className="w-5 h-6 text-center bg-slate-50 border border-slate-200 rounded text-[12px] font-mono outline-none focus:border-primary"
|
||||
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>
|
||||
@@ -481,7 +576,7 @@ export function PdfPreviewTest({
|
||||
{/* 略缩图列表 */}
|
||||
<div
|
||||
ref={thumbsPanelRef}
|
||||
className="flex-1 overflow-y-auto py-2 px-2 space-y-2"
|
||||
className="flex-1 overflow-y-auto py-2 px-2"
|
||||
>
|
||||
{numPages === null ? (
|
||||
<div className="text-center text-[11px] text-slate-400 py-4">加载中…</div>
|
||||
@@ -491,76 +586,81 @@ export function PdfPreviewTest({
|
||||
<div className="mt-1">此规则无关联页面</div>
|
||||
</div>
|
||||
) : (
|
||||
thumbPages.map(p => {
|
||||
const info = pageStatusMap.get(p);
|
||||
const isCur = p === currentPage;
|
||||
const isRulePage = rulePages.includes(p);
|
||||
<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>
|
||||
);
|
||||
}
|
||||
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';
|
||||
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="block w-full group"
|
||||
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={112}
|
||||
renderTextLayer={false}
|
||||
renderAnnotationLayer={false}
|
||||
loading={<div className="w-full h-[150px] bg-slate-50" />}
|
||||
error={<div className="w-full h-[150px] bg-slate-50" />}
|
||||
/>
|
||||
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>
|
||||
{badge}
|
||||
</div>
|
||||
<div
|
||||
className={`text-center text-[10.5px] mt-1 ${
|
||||
isCur ? 'text-[#00684a] font-semibold' : 'text-slate-500 group-hover:text-slate-700'
|
||||
}`}
|
||||
);
|
||||
}
|
||||
|
||||
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} 页`}
|
||||
>
|
||||
{p}
|
||||
</div>
|
||||
{fieldsLabel}
|
||||
</button>
|
||||
);
|
||||
})
|
||||
<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>
|
||||
@@ -586,7 +686,7 @@ export function PdfPreviewTest({
|
||||
<Page
|
||||
pageNumber={Math.min(currentPage, numPages)}
|
||||
scale={zoomLevel / 100}
|
||||
devicePixelRatio={typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1}
|
||||
devicePixelRatio={mainPageDevicePixelRatio}
|
||||
renderTextLayer={false}
|
||||
renderAnnotationLayer={false}
|
||||
onLoadSuccess={onMainPageLoadSuccess}
|
||||
@@ -610,7 +710,8 @@ export function PdfPreviewTest({
|
||||
y={mainHighlight.y}
|
||||
width={mainHighlight.width}
|
||||
height={mainHighlight.height}
|
||||
fill="#00AA00"
|
||||
// fill="#00AA00"
|
||||
fill="#abf694"
|
||||
fillOpacity="0.1"
|
||||
stroke="#00684a"
|
||||
strokeWidth="0.5"
|
||||
|
||||
Reference in New Issue
Block a user