// 查看切片 import React, { useEffect, useMemo, useState, useRef, useLayoutEffect } from 'react'; import { Drawer, Row, Col, Input, Button, List, Typography, Select, Spin, Image, message } from 'antd'; import type { DrawerProps, RadioChangeEvent } from 'antd'; import { SnippetsOutlined } from '@ant-design/icons'; import MarkdownIt from 'markdown-it'; import { fetchReviseToolAllList, fetchReviseToolSliceList, apis } from '@/apis'; import '../../revisionTool/components/reviseDrawer.less' import { Document, Page, pdfjs } from 'react-pdf'; interface ReviseDrawerProps { openDrawer: boolean; onClose: () => void; title: string; record: any; } // 设置 pdf.worker(使用 CDN 作为回退) try { pdfjs.GlobalWorkerOptions.workerSrc = (pdfjs.GlobalWorkerOptions.workerSrc || '') || '//cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.worker.min.js'; } catch (e) { // ignore } // =================== PDF 子组件(文件级,最重要) =================== const LazyPage = React.memo( ({ pageNumber, width, containerRef, }: { pageNumber: number; width: number; containerRef: React.RefObject; }) => { const elRef = React.useRef(null); const [visible, setVisible] = React.useState(false); React.useEffect(() => { const el = elRef.current; const root = containerRef.current; if (!el || !root) return; const io = new IntersectionObserver( entries => { entries.forEach(e => e.isIntersecting && setVisible(true)); }, { root, rootMargin: '400px' } ); io.observe(el); return () => io.disconnect(); }, [containerRef]); return (
{visible ? ( ) : (
)}
); } ); const PdfViewer = React.memo( ({ file, numPages, pageWidth, scale, onLoadSuccess, containerRef, }: { file: string; numPages: number; pageWidth: number; scale: number; onLoadSuccess: any; containerRef: React.RefObject; }) => { return ( {Array.from({ length: numPages }).map((_, idx) => ( ))} ); }, (prev, next) => prev.file === next.file && prev.numPages === next.numPages && prev.pageWidth === next.pageWidth && prev.scale === next.scale ); const ReviseDrawer: React.FC = (props: ReviseDrawerProps) => { const { openDrawer, onClose, title, record } = props; const [placement, setPlacement] = useState('right'); const onGetSliceListByDocumentId = async (documentId: string) => { try { const res: any = await apis.getSliceListByDocumentId(documentId); if (res && res.data && Array.isArray(res.data)) { const results = res.data; results.forEach((item: any) => { item.sliceText = customRender(item.sliceText, item.mediaList) }) setRightSlices(results); } else { return []; } } catch (err) { return []; } }; useEffect(() => { if (openDrawer) { console.log('预览切片record', record); onGetSliceListByDocumentId(record.documentId); } }, [openDrawer]); const [leftSearch, setLeftSearch] = useState('');// 左边搜索 const [rightSearch, setRightSearch] = useState(''); // 右边搜索 const [leftSpliceSearch, setLeftSpliceSearch] = useState('');// 左边切片搜索 const [rightSpliceSearch, setRightSpliceSearch] = useState('');// 右边切片搜索 const [revisionOptions, setRevisionOptions] = useState([]); const [revisionLoading, setRevisionLoading] = useState(false); const [selectedRevision, setSelectedRevision] = useState(undefined); const [leftDocumentId, setLeftDocumentId] = useState(''); // 左边选中的文档ID const [rightDocumentId, setRightDocumentId] = useState(''); // 右边选中的文档ID const [leftOptions, setLeftOptions] = useState([]);// 左边数据 const [rightOptions, setRightOptions] = useState([]);// 右边数据 const [leftLoading, setLeftLoading] = useState(false);// 左边加载状态 const [rightLoading, setRightLoading] = useState(false);// 右边加载状态 const [selectedStandard, setSelectedStandard] = useState(null); const [leftSlices, setLeftSlices] = useState([]);// 左边切片列表 const [rightSlices, setRightSlices] = useState([]);// 右边切片列表 const [leftSlicesLoading, setLeftSlicesLoading] = useState(false);// 左边切片加载状态 const [rightSlicesLoading, setRightSlicesLoading] = useState(false);// 右边切片加载状态 const [pdfBlobUrl, setPdfBlobUrl] = useState(null); const [pdfLoading, setPdfLoading] = useState(false); const [largeFile, setLargeFile] = useState(false); const [pageNumber, setPageNumber] = useState(1); const [scale, setScale] = useState(1.0); const containerRef = useRef(null); const [pageWidth, setPageWidth] = useState(600); const marked = new MarkdownIt({ html: true, typographer: true }); useEffect(() => { if (leftDocumentId) { onFetchReviseToolSliceList(leftDocumentId, 'left'); } }, [leftDocumentId]); useEffect(() => { if (rightDocumentId) { onFetchReviseToolSliceList(rightDocumentId, 'right'); } }, [rightDocumentId]); const onChange = (e: RadioChangeEvent) => { setPlacement(e.target.value); }; const onFetchTakaiAppTypeListApi = async () => { setRevisionLoading(true); try { const res: any = await apis.fetchTakaiAppTypeList('revision_status'); if (res && res.data && Array.isArray(res.data)) { const opts = res.data.map((it: any) => ({ label: it.dictLabel, value: it.dictValue })); setSelectedRevision(opts[0].value); setRevisionOptions(opts); } else { setRevisionOptions([]); } } catch (err) { setRevisionOptions([]); } finally { setRevisionLoading(false); } }; useEffect(() => { onFetchTakaiAppTypeListApi(); }, []); const customRender = (text: string, mdImgUrlList: any[]) => { // 比如:把 "我是图片" 替换成一张图片 mdImgUrlList?.forEach(item => { text = text.replace(new RegExp(item.originText, 'g'), `${item.originText}`); }) return text; } // 获取知识库修订工具切片列表 const onFetchReviseToolSliceList = async (documentId: string, side?: 'left' | 'right', sliceText?: string) => { setLeftSlicesLoading(side === 'left' ? true : leftSlicesLoading); setRightSlicesLoading(side === 'right' ? true : rightSlicesLoading); // Implement the function to fetch revise tool slice list const res: any = await fetchReviseToolSliceList({ documentId: documentId || '', sliceText: sliceText || '' }); // 假设接口返回结构为 { code: 200, data: [ { sliceContent }, ... ] } if (res && res.data && Array.isArray(res.data)) { const results = res.data; results.forEach((item: any) => { item.sliceText = customRender(item.sliceText, item.mediaList) }) if (side === 'left') { setLeftSlicesLoading(false); setLeftSlices(results); } else if (side === 'right') { setRightSlicesLoading(false); setRightSlices(results); } } else { if (side === 'left') setLeftSlices([]); else if (side === 'right') setRightSlices([]); } } const [LeftActiveId, setLeftActiveId] = useState(null); const [RightActiveId, setRightActiveId] = useState(null); const [subLoading, setSubLoading] = useState(false); const onAllClose = () => { setLeftActiveId(null); setRightActiveId(null); setLeftSearch(''); setRightSearch(''); setLeftSpliceSearch(''); setRightSpliceSearch(''); setLeftDocumentId(''); setRightDocumentId(''); setLeftOptions([]); setRightOptions([]); setLeftSlices([]); setRightSlices([]); onClose(); } const [numPages, setNumPages] = useState(0); function onLoadSuccess({ numPages }: { numPages: number }) { setNumPages(numPages); setPageNumber(1); } // 测量容器宽度以计算 Page 渲染宽度 useLayoutEffect(() => { const update = () => { const el = containerRef.current; if (el) { const w = el.clientWidth - 20; // 留白 setPageWidth(Math.max(200, w)); } }; update(); window.addEventListener('resize', update); return () => window.removeEventListener('resize', update); }, []); // Fetch PDF as blob to avoid browser auto-download from Content-Disposition useEffect(() => { let cancelled = false; let currentBlobUrl: string | null = null; const fetchBlob = async () => { if (!record?.pdfUrl) { setPdfBlobUrl(null); return; } try { // 尝试先用 HEAD 获取文件大小,若大文件则跳过下载 blob,改用原始 URL 由 pdf.js 处理流式加载 const headResp = await fetch(record.pdfUrl, { method: 'HEAD', mode: 'cors' }); if (headResp && headResp.ok) { const len = headResp.headers.get('Content-Length'); if (len) { const size = parseInt(len, 10); const threshold = 10 * 1024 * 1024; // 10MB if (!Number.isNaN(size) && size > threshold) { setLargeFile(true); // 不下载 blob,直接让 Document 使用原始 URL(支持 range 请求的服务器会更快) setPdfBlobUrl(null); return; } } } setPdfLoading(true); const res = await fetch(record.pdfUrl, { method: 'GET', mode: 'cors' }); if (!res.ok) throw new Error('fetch failed'); const blob = await res.blob(); currentBlobUrl = URL.createObjectURL(blob); if (!cancelled) { setPdfBlobUrl(prev => { if (prev) URL.revokeObjectURL(prev); return currentBlobUrl; }); } else { if (currentBlobUrl) URL.revokeObjectURL(currentBlobUrl); } } catch (err) { console.error('fetch pdf blob error', err); setPdfBlobUrl(null); } finally { if (!cancelled) setPdfLoading(false); } }; fetchBlob(); return () => { cancelled = true; if (currentBlobUrl) URL.revokeObjectURL(currentBlobUrl); }; }, [record?.pdfUrl]); return (
{/* Left column */}
{Math.round(scale * 100)}%
{pdfLoading ? (
PDF 加载中...
) : record?.pdfUrl ? ( (pdfBlobUrl || record.pdfUrl) ? ( ) : (
无法生成预览,
) ) : (
未找到 PDF
)}
{/* Right column (same as left) */}
{rightSlices.length > 0 ?
{rightSlices.map((module: any, index) => (
setRightActiveId(module.sliceId)} > {module.sliceText && (() => { const html = marked.render(module.sliceText || ''); const parts: any[] = []; const imgRegex = /]*src=["']([^"']+)["'][^>]*>/g; let lastIndex = 0; let match: RegExpExecArray | null; while ((match = imgRegex.exec(html)) !== null) { const idx = match.index; const textSeg = html.substring(lastIndex, idx); if (textSeg) parts.push(
); const url = match[1]; parts.push(); lastIndex = idx + match[0].length; } const rest = html.substring(lastIndex); if (rest) parts.push(
); return
{parts}
; })()}
))}
:
暂无数据
}
) } export default observer(ReviseDrawer);