| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437 |
- // 查看切片
- 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<HTMLDivElement>;
- }) => {
- const elRef = React.useRef<HTMLDivElement | null>(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 (
- <div ref={elRef} style={{ background: '#fff' }}>
- {visible ? (
- <Page
- pageNumber={pageNumber}
- // width={width}
- renderTextLayer={false}
- renderAnnotationLayer={false}
- className='pdf-page'
- />
- ) : (
- <div
- style={{
- width: '100%',
- height: Math.floor(width * 1.3),
- background: '#fafafa',
- }}
- />
- )}
- </div>
- );
- }
- );
- const PdfViewer = React.memo(
- ({
- file,
- numPages,
- pageWidth,
- scale,
- onLoadSuccess,
- containerRef,
- }: {
- file: string;
- numPages: number;
- pageWidth: number;
- scale: number;
- onLoadSuccess: any;
- containerRef: React.RefObject<HTMLDivElement>;
- }) => {
- return (
- <Document
- // renderMode="canvas"
- file={file}
- onLoadSuccess={onLoadSuccess}
- >
- {Array.from({ length: numPages }).map((_, idx) => (
- <LazyPage
- key={idx}
- pageNumber={idx + 1}
- width={Math.floor(pageWidth * scale)}
- containerRef={containerRef}
- />
- ))}
- </Document>
- );
- },
- (prev, next) =>
- prev.file === next.file &&
- prev.numPages === next.numPages &&
- prev.pageWidth === next.pageWidth &&
- prev.scale === next.scale
- );
- const ReviseDrawer: React.FC<ReviseDrawerProps> = (props: ReviseDrawerProps) => {
- const { openDrawer, onClose, title, record } = props;
- const [placement, setPlacement] = useState<DrawerProps['placement']>('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<any[]>([]);
- const [revisionLoading, setRevisionLoading] = useState(false);
- const [selectedRevision, setSelectedRevision] = useState<string | undefined>(undefined);
- const [leftDocumentId, setLeftDocumentId] = useState(''); // 左边选中的文档ID
- const [rightDocumentId, setRightDocumentId] = useState(''); // 右边选中的文档ID
- const [leftOptions, setLeftOptions] = useState<any[]>([]);// 左边数据
- const [rightOptions, setRightOptions] = useState<any[]>([]);// 右边数据
- const [leftLoading, setLeftLoading] = useState(false);// 左边加载状态
- const [rightLoading, setRightLoading] = useState(false);// 右边加载状态
- const [selectedStandard, setSelectedStandard] = useState<string | null>(null);
- const [leftSlices, setLeftSlices] = useState<string[]>([]);// 左边切片列表
- const [rightSlices, setRightSlices] = useState<string[]>([]);// 右边切片列表
- const [leftSlicesLoading, setLeftSlicesLoading] = useState(false);// 左边切片加载状态
- const [rightSlicesLoading, setRightSlicesLoading] = useState(false);// 右边切片加载状态
- const [pdfBlobUrl, setPdfBlobUrl] = useState<string | null>(null);
- const [pdfLoading, setPdfLoading] = useState(false);
- const [largeFile, setLargeFile] = useState(false);
- const [pageNumber, setPageNumber] = useState<number>(1);
- const [scale, setScale] = useState<number>(1.0);
- const containerRef = useRef<HTMLDivElement | null>(null);
- const [pageWidth, setPageWidth] = useState<number>(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'), `<img src="${item.mediaUrl}" alt="${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<string | null>(null);
- const [RightActiveId, setRightActiveId] = useState<string | null>(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<number>(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 (
- <Drawer
- title={title}
- placement={placement}
- closable={true}
- onClose={onAllClose}
- open={openDrawer}
- width={'90%'}
- >
- <div className="flex gap-4 h-full">
- {/* Left column */}
- <div className="w-1/2 border-r pr-4">
- <Spin spinning={leftSlicesLoading || pdfLoading} className="h-full">
- <div className="flex flex-col h-full">
- <div className="mb-2 flex items-center justify-end">
- <div className="flex items-center gap-2">
- <Button size="small" onClick={() => setScale(s => Math.max(0.5, s - 0.1))}>-</Button>
- <span className="text-sm">{Math.round(scale * 100)}%</span>
- <Button size="small" onClick={() => setScale(s => Math.min(2, s + 0.1))}>+</Button>
- <Button size="small" onClick={() => { if (record?.pdfUrl) window.open(record.pdfUrl, '_blank'); }}>在新窗口打开</Button>
- </div>
- </div>
- <div
- ref={containerRef}
- className="overflow-y-auto"
- style={{ maxHeight: 'calc(100vh - 130px)' }}
- >
- {pdfLoading ? (
- <div className="text-center py-8">PDF 加载中...</div>
- ) : record?.pdfUrl ? (
- (pdfBlobUrl || record.pdfUrl) ? (
- <PdfViewer
- file={pdfBlobUrl || record.pdfUrl}
- numPages={numPages}
- pageWidth={pageWidth}
- scale={scale}
- onLoadSuccess={onLoadSuccess}
- containerRef={containerRef}
- />
- ) : (
- <div className="text-center py-8">
- 无法生成预览,
- <Button
- type="link"
- onClick={() => {
- if (record?.pdfUrl) window.open(record.pdfUrl, '_blank');
- }}
- >
- 在新窗口打开
- </Button>
- </div>
- )
- ) : (
- <div className="text-center text-gray-500 mt-10">未找到 PDF</div>
- )}
- </div>
- </div>
- </Spin>
- </div>
- {/* Right column (same as left) */}
- <div className="w-1/2">
- <Spin spinning={rightSlicesLoading} className='h-full'>
- {rightSlices.length > 0 ? <div className="overflow-y-auto" style={{ maxHeight: 'calc(100vh - 130px)' }}>
- {rightSlices.map((module: any, index) => (
- <div
- key={`r-${index}`}
- className={`relative px-3 py-2 text-sm leading-7 transition-all duration-200 cursor-pointer
- ${index !== rightSlices.length - 1 ? 'border-b border-gray-100' : ''
- }`}
- onClick={() => setRightActiveId(module.sliceId)}
- >
- {module.sliceText && (() => {
- const html = marked.render(module.sliceText || '');
- const parts: any[] = [];
- const imgRegex = /<img[^>]*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(<div key={`rt-${index}-${lastIndex}`} dangerouslySetInnerHTML={{ __html: textSeg }} />);
- const url = match[1];
- parts.push(<Image key={`rimg-${index}-${idx}`} src={url} preview={{}} style={{ maxWidth: '100%' }} />);
- lastIndex = idx + match[0].length;
- }
- const rest = html.substring(lastIndex);
- if (rest) parts.push(<div key={`rt-last-${index}`} dangerouslySetInnerHTML={{ __html: rest }} />);
- return <div className="text-sm leading-7 text-gray-800 markdown-preview">{parts}</div>;
- })()}
- </div>
- ))}
- </div> : <div className="text-center text-gray-500 mt-10">暂无数据</div>}
- </Spin>
- </div>
- </div>
- </Drawer>
- )
- }
- export default observer(ReviseDrawer);
|