|
|
@@ -1,1573 +0,0 @@
|
|
|
-import React, {
|
|
|
- useCallback,
|
|
|
- useEffect,
|
|
|
- useMemo,
|
|
|
- useRef,
|
|
|
- useState,
|
|
|
-} from 'react';
|
|
|
-import {
|
|
|
- Button,
|
|
|
- Empty,
|
|
|
- Modal,
|
|
|
- Drawer,
|
|
|
- Space,
|
|
|
- Spin,
|
|
|
- Typography,
|
|
|
- Input,
|
|
|
- message,
|
|
|
- Upload,
|
|
|
- UploadProps,
|
|
|
- Checkbox,
|
|
|
- Tooltip,
|
|
|
- Popconfirm,
|
|
|
- Image,
|
|
|
- Tabs
|
|
|
-} from 'antd';
|
|
|
-import { SearchOutlined, MenuFoldOutlined, MenuUnfoldOutlined, FileTextOutlined, DeleteOutlined, QuestionOutlined, QuestionCircleOutlined, FullscreenOutlined } from '@ant-design/icons';
|
|
|
-import MarkdownIt from 'markdown-it';
|
|
|
-
|
|
|
-import { apis } from '@/apis';
|
|
|
-import config, { getHeaders } from '@/apis/config';
|
|
|
-import './style.less'
|
|
|
-import { Document, Page, pdfjs } from 'react-pdf';
|
|
|
-import workerSrc from '@/assets/pdf.worker.min.js?url';
|
|
|
-import 'react-pdf/dist/Page/TextLayer.css';
|
|
|
-import 'react-pdf/dist/Page/AnnotationLayer.css';
|
|
|
-import { Console } from 'console';
|
|
|
-pdfjs.GlobalWorkerOptions.workerSrc = workerSrc;
|
|
|
-// pdfjs.GlobalWorkerOptions.workerSrc = new URL(
|
|
|
-// 'pdfjs-dist/build/pdf.worker.min.mjs',
|
|
|
-// import.meta.url,
|
|
|
-// ).toString();
|
|
|
-import zHuifu from '@/assets/public/z-huifu.svg'
|
|
|
-import zJiexi from '@/assets/public/z-jiexi.svg'
|
|
|
-import zQiefen from '@/assets/public/z-qiefen.svg'
|
|
|
-import zXinzeng from '@/assets/public/z-xinzeng.svg'
|
|
|
-import zHebing from '@/assets/public/z-hebing.svg'
|
|
|
-import zTuozhan from '@/assets/public/z-tuozhan.svg'
|
|
|
-import zDuoban from '@/assets/public/z-duoban.svg'
|
|
|
-import tubing from '@/assets/public/tubing.png'
|
|
|
-import rfq from '@/assets/public/rfq.png'
|
|
|
-const { TextArea } = Input;
|
|
|
-
|
|
|
-const marked = new MarkdownIt({ html: true, typographer: true });
|
|
|
-
|
|
|
-type KnowledgeRecord = {
|
|
|
- knowledgeId: string;
|
|
|
- name: string;
|
|
|
- length?: number;
|
|
|
- wordNum?: number;
|
|
|
- documentSize?: number;
|
|
|
- markUrl?: string;
|
|
|
- previewUrl?: string;
|
|
|
- url?: string;
|
|
|
- [key: string]: any;
|
|
|
- status?: string;
|
|
|
- standardClassification?: string
|
|
|
- suffix: string
|
|
|
-};
|
|
|
-
|
|
|
-interface ModuleItem {
|
|
|
- id: string;
|
|
|
- content: string;
|
|
|
- mdContent: string;
|
|
|
- sliceId?: string;
|
|
|
- saveLoading?: boolean;
|
|
|
- delLoading?: boolean;
|
|
|
- mediaList?: any[];
|
|
|
- uploading?: boolean;
|
|
|
-}
|
|
|
-
|
|
|
-const initialModules: ModuleItem[] = [
|
|
|
- {
|
|
|
- id: 'm1',
|
|
|
- content:
|
|
|
- '### 新增切片',
|
|
|
- mdContent: '### 新增切片'
|
|
|
- }
|
|
|
-];
|
|
|
-interface MdModalProps {
|
|
|
- open: boolean;
|
|
|
- detailDocument?: any;
|
|
|
- knowledgeDetail?: any;
|
|
|
- onCancel: (type?: number) => void;
|
|
|
-}
|
|
|
-
|
|
|
-import store from './store';
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-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 MdModal: React.FC<MdModalProps> = (props) => {
|
|
|
-
|
|
|
- const {
|
|
|
- state,
|
|
|
- onSetStarts
|
|
|
- } = store;
|
|
|
- useEffect(() => {
|
|
|
- loadKnowledgeList(1, '');
|
|
|
- }, [state.starts])
|
|
|
-
|
|
|
- const { open, onCancel, detailDocument, knowledgeDetail } = props;
|
|
|
- const [documents, setDocuments] = useState<KnowledgeRecord[]>([]);
|
|
|
- const [selectedDoc, setSelectedDoc] = useState<KnowledgeRecord | null>(null); // 当前选中的文件
|
|
|
- const selectedRefDoc = useRef<KnowledgeRecord | null>(null); // 当前选中的文件
|
|
|
- const [pagination, setPagination] = useState({
|
|
|
- pageNum: 1,
|
|
|
- pageSize: 20,
|
|
|
- total: 0,
|
|
|
- });
|
|
|
- const [listLoading, setListLoading] = useState(false);
|
|
|
- const [sliceLoading, setSliceLoading] = useState(true);
|
|
|
- const [pdfLoading, setPdfLoading] = useState(true);
|
|
|
- const listLoadingRef = useRef(false);
|
|
|
- const [hasMore, setHasMore] = useState(true);
|
|
|
- const listContainerRef = useRef<HTMLDivElement>(null);
|
|
|
- // 切片列表
|
|
|
- const [modules, setModules] = useState<ModuleItem[]>([]);
|
|
|
- const [oneModules, setOneModules] = useState('');
|
|
|
- // 默认选中第一条数据
|
|
|
- const [activeId, setActiveId] = useState<string | null>(initialModules[0]?.id);
|
|
|
- const [fromEditor, setFromEditor] = useState(true);
|
|
|
- const editorContainerRef = useRef<HTMLDivElement>(null);
|
|
|
- const previewContainerRef = useRef<HTMLDivElement>(null);
|
|
|
- const [keyword, setKeyword] = useState('');
|
|
|
- const [isListCollapsed, setIsListCollapsed] = useState(false);// 文档列表是否展开
|
|
|
- const [isPDfCollapsed, setIsPDfCollapsed] = useState(false);// PDF预览是否展开
|
|
|
- const [delSliceIds, setDelSliceIds] = useState<string[]>([]);
|
|
|
- const [selectedModuleIds, setSelectedModuleIds] = useState<string[]>([]); // 用于合并的多选模块
|
|
|
-
|
|
|
- const [pdfBuffer, setPdfBuffer] = useState<any>(null);
|
|
|
- const [numPages, setNumPages] = useState<number>(0);
|
|
|
- // 全屏预览状态与缩放
|
|
|
- const [pdfFullVisible, setPdfFullVisible] = useState<boolean>(false);
|
|
|
- const [pdfFullScale, setPdfFullScale] = useState<number>(1.5);
|
|
|
-
|
|
|
- const [currentDocumentId, setCurrentDocumentId] = useState<string | null>(null);
|
|
|
- const pdfScrollRef = useRef<HTMLDivElement | null>(null);
|
|
|
- const [pageWidth, setPageWidth] = useState<number>(600);
|
|
|
-
|
|
|
- // 替换渲染数据
|
|
|
- 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 sliceDataProcess = (data: any[]) => {
|
|
|
- data.forEach((item: any) => {
|
|
|
- item.content = item.sliceText
|
|
|
- item.id = item.sliceId,
|
|
|
- item.mdContent = customRender(item.sliceText, item.mediaList)
|
|
|
- })
|
|
|
- setModules(data || [])
|
|
|
- }
|
|
|
- // 获取切片列表
|
|
|
- const onGetSliceListByDocumentId = async (item: any) => {
|
|
|
- const res: any = await apis.getSliceListByDocumentId(item.documentId)
|
|
|
- if (res.code === 200 && res.data) {
|
|
|
- if (res.data.length > 0) {
|
|
|
- res.data.forEach((item: any) => {
|
|
|
- item.saveLoading = false;
|
|
|
- item.delLoading = false;
|
|
|
- item.uploading = false;
|
|
|
- })
|
|
|
- sliceDataProcess(res.data)
|
|
|
- } else {
|
|
|
- setModules(initialModules)
|
|
|
- }
|
|
|
- setSliceLoading(false);
|
|
|
- }
|
|
|
- }
|
|
|
- // 获取MD原文数据
|
|
|
- const onGetMarkDownSliceByDocumentId = async (documentId: string) => {
|
|
|
- const res: any = await apis.getMarkDodnSliceByDocumentId(documentId);
|
|
|
- if (res.code === 200 && res.data) {
|
|
|
- setOneModules(res.data[0] || '');
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 获取文档详情
|
|
|
- const [customSeparator, setCustomSeparator] = useState<string>('');
|
|
|
- const onFetchDocumentDetailLibApi = async (documentId: string) => {
|
|
|
- const res: any = await apis.fetchTakaiDocumentDetailLibApi(documentId);
|
|
|
- if (res.code === 200 && res.data) {
|
|
|
- setQaChecked(res.data.qaChecked || false);
|
|
|
- setRelatedQuestionsEnabled(res.data.relatedQuestionsEnabled || false);
|
|
|
- setSummaryGenerationEnabled(res.data.summaryGenerationEnabled || false);
|
|
|
- setParentGenerationEnabled(res.data.parentGenerationEnabled || false);
|
|
|
- setCustomSeparator(res.data.customSeparator || '');
|
|
|
- }
|
|
|
- }
|
|
|
- // 获取PDF
|
|
|
- const onExportPdfStream = async (item: any) => {
|
|
|
- setModules([]) // 清空切片
|
|
|
- setPdfBuffer(null);// 清空PDF预览
|
|
|
- setTabKey('slices') // 清空智能切片模式
|
|
|
- setSelectedModuleIds([]); // 清空合并切片以及需要删除的切片
|
|
|
- setSliceLoading(true)
|
|
|
- setSelectedDoc(item);
|
|
|
- selectedRefDoc.current = item;
|
|
|
- setCurrentDocumentId(item.documentId);
|
|
|
- setPdfLoading(false);
|
|
|
- if (item?.suffix !== 'md') {
|
|
|
- setPdfLoading(true);
|
|
|
- const ids = [item.documentId]
|
|
|
- const blob = await apis.exportPdfStream(ids)
|
|
|
- setPdfLoading(false);
|
|
|
- const url = URL.createObjectURL(blob);
|
|
|
- setPdfBuffer(url);
|
|
|
- }
|
|
|
-
|
|
|
- onGetSliceListByDocumentId(item)
|
|
|
- onGetMarkDownSliceByDocumentId(item.documentId);
|
|
|
- onFetchDocumentDetailLibApi(item.documentId);
|
|
|
- }
|
|
|
- // 计算渲染宽度以降低每页 canvas 分辨率开销(与 prevewSlice 行为一致)
|
|
|
- useEffect(() => {
|
|
|
- const update = () => {
|
|
|
- const el = pdfScrollRef.current;
|
|
|
- if (el) {
|
|
|
- const w = el.clientWidth - 20;
|
|
|
- setPageWidth(Math.max(200, w));
|
|
|
- }
|
|
|
- };
|
|
|
- update();
|
|
|
- window.addEventListener('resize', update);
|
|
|
- return () => window.removeEventListener('resize', update);
|
|
|
- }, []);
|
|
|
-
|
|
|
- //PDF渲染列表
|
|
|
- const renderPdfPreview = () => {
|
|
|
- function onLoadSuccess({ numPages }: { numPages: number }) {
|
|
|
- setNumPages(numPages);
|
|
|
- }
|
|
|
- if (!pdfBuffer) {
|
|
|
- return (
|
|
|
- <div className="flex h-full items-center justify-center">
|
|
|
- <Empty description="请选择左侧的文档" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
|
|
- </div>
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- return (
|
|
|
- <div>
|
|
|
- <style>{`
|
|
|
- .react-pdf__Page__canvas {
|
|
|
- width: 100% !important;
|
|
|
- padding-left:0 !important;
|
|
|
- height: auto !important;
|
|
|
- }
|
|
|
- .react-pdf__Page__textContent,.annotationLayer {
|
|
|
- padding:0;
|
|
|
- margin:0;
|
|
|
- width: 100% !important;
|
|
|
- padding-left:0 !important;
|
|
|
- height: auto !important;
|
|
|
- }
|
|
|
- `}</style>
|
|
|
- {pdfBuffer && (
|
|
|
- <Document file={pdfBuffer}
|
|
|
- renderMode='canvas'
|
|
|
- onLoadSuccess={onLoadSuccess}
|
|
|
- loading={<div className="text-center py-8">加载中...</div>}
|
|
|
- >
|
|
|
- {Array.from(new Array(numPages), (_, idx) => (
|
|
|
- // <Page key={idx} pageNumber={idx + 1}
|
|
|
- // renderAnnotationLayer={false}
|
|
|
- // renderTextLayer={true}
|
|
|
- // />
|
|
|
- <LazyPage key={`page-${idx}`} pageNumber={idx + 1} width={Math.floor(pageWidth)} containerRef={pdfScrollRef} />
|
|
|
- ))}
|
|
|
- </Document>
|
|
|
- )}
|
|
|
- </div>)
|
|
|
- }
|
|
|
- // 获取文档列表
|
|
|
- const loadKnowledgeList = useCallback(
|
|
|
- async (targetPage = 1, searchKeyword?: string) => {
|
|
|
- if (listLoadingRef.current) return;
|
|
|
- listLoadingRef.current = true;
|
|
|
- setListLoading(true);
|
|
|
- try {
|
|
|
- const res: any = await apis.sliceDocumentList({
|
|
|
- pageNum: targetPage,
|
|
|
- pageSize: pagination.pageSize,
|
|
|
- knowledge_id: knowledgeDetail.knowledgeId,
|
|
|
- name: searchKeyword !== undefined ? searchKeyword : ''
|
|
|
- });
|
|
|
- const rows = res?.rows || [];
|
|
|
- setDocuments((prev) => {
|
|
|
- if (targetPage === 1) {
|
|
|
- const firstDoc = selectedDoc || detailDocument;
|
|
|
- if (firstDoc && firstDoc.documentId) {
|
|
|
- const isExist = rows.filter((item: any) => item.documentId !== firstDoc.documentId);
|
|
|
- return [firstDoc, ...isExist];
|
|
|
- }
|
|
|
- return rows;
|
|
|
- } else {
|
|
|
- return [...prev, ...rows.filter((row: KnowledgeRecord) => prev.every((p) => p.knowledgeId !== row.knowledgeId))]
|
|
|
- }
|
|
|
- });
|
|
|
- setPagination((prev) => ({
|
|
|
- ...prev,
|
|
|
- pageNum: targetPage,
|
|
|
- total: res?.total || 0,
|
|
|
- }));
|
|
|
- setHasMore(targetPage * pagination.pageSize < (res?.total || 0));
|
|
|
- } catch (error: any) {
|
|
|
- message.error(error?.msg || '获取文档列表失败');
|
|
|
- } finally {
|
|
|
- listLoadingRef.current = false;
|
|
|
- setListLoading(false);
|
|
|
- }
|
|
|
- },
|
|
|
- [pagination.pageSize, detailDocument, selectedDoc]
|
|
|
- );
|
|
|
-
|
|
|
- useEffect(() => {
|
|
|
- loadKnowledgeList(1, '');
|
|
|
- onExportPdfStream(detailDocument);
|
|
|
- }, [detailDocument]);
|
|
|
- // 列表滚动加载更多
|
|
|
- const handleListScroll = useCallback(() => {
|
|
|
- const container = listContainerRef.current;
|
|
|
- if (
|
|
|
- !container ||
|
|
|
- listLoadingRef.current ||
|
|
|
- !hasMore ||
|
|
|
- documents.length === 0
|
|
|
- ) {
|
|
|
- return;
|
|
|
- }
|
|
|
- const nearBottom =
|
|
|
- container.scrollTop + container.clientHeight >=
|
|
|
- container.scrollHeight - 40;
|
|
|
- if (nearBottom) {
|
|
|
- loadKnowledgeList(pagination.pageNum + 1);
|
|
|
- }
|
|
|
- }, [documents.length, hasMore, loadKnowledgeList, pagination.pageNum]);
|
|
|
-
|
|
|
- useEffect(() => {
|
|
|
- const container = listContainerRef.current;
|
|
|
- if (!container) return;
|
|
|
- container.addEventListener('scroll', handleListScroll);
|
|
|
- return () => container.removeEventListener('scroll', handleListScroll);
|
|
|
- }, [handleListScroll, isListCollapsed]);
|
|
|
- // 最后两列滚动
|
|
|
- const scrollToElement = (
|
|
|
- containerRef: React.RefObject<HTMLDivElement>,
|
|
|
- element: HTMLElement | null
|
|
|
- ) => {
|
|
|
- if (!containerRef.current || !element) return;
|
|
|
- const containerTop = containerRef.current.getBoundingClientRect().top;
|
|
|
- const elemTop = element.getBoundingClientRect().top;
|
|
|
- containerRef.current.scrollBy({
|
|
|
- top: elemTop - containerTop - 16,
|
|
|
- behavior: 'smooth',
|
|
|
- });
|
|
|
- };
|
|
|
-
|
|
|
- const setActiveModule = (id: string, editorSide: boolean) => {
|
|
|
- setActiveId(id);
|
|
|
- setFromEditor(editorSide);
|
|
|
- };
|
|
|
-
|
|
|
- useEffect(() => {
|
|
|
- if (!activeId) return;
|
|
|
- const targetEditorModule = editorContainerRef.current?.querySelector(
|
|
|
- `[data-id="${activeId}"]`
|
|
|
- ) as HTMLElement | null;
|
|
|
- const targetPreviewModule = previewContainerRef.current?.querySelector(
|
|
|
- `[data-id="${activeId}"]`
|
|
|
- ) as HTMLElement | null;
|
|
|
- if (fromEditor && targetPreviewModule) {
|
|
|
- scrollToElement(previewContainerRef, targetPreviewModule);
|
|
|
- }
|
|
|
- if (!fromEditor && targetEditorModule) {
|
|
|
- scrollToElement(editorContainerRef, targetEditorModule);
|
|
|
- }
|
|
|
- }, [activeId, fromEditor]);
|
|
|
- // 输入框选中操作
|
|
|
- const handleContentChange = (id: string, newContent: string, newMediaList?: any) => {
|
|
|
- setModules((prev) =>
|
|
|
- prev.map((module: any) => {
|
|
|
- if (module.id === id) {
|
|
|
- if (newMediaList) {
|
|
|
- return { ...module, content: newContent, mdContent: customRender(newContent, newMediaList || module.mediaList), mediaList: newMediaList || module.mediaList }
|
|
|
- } else {
|
|
|
- return { ...module, content: newContent, mdContent: customRender(newContent, module.mediaList) }
|
|
|
- }
|
|
|
- } else {
|
|
|
- return module
|
|
|
- }
|
|
|
- }));
|
|
|
- };
|
|
|
- const setSaveLoadingChange = (id: string, flag: boolean) => {
|
|
|
- setModules((prev) =>
|
|
|
- prev.map((module: any) => {
|
|
|
- if (module.id === id) {
|
|
|
- return { ...module, saveLoading: flag }
|
|
|
- } else {
|
|
|
- return module
|
|
|
- }
|
|
|
- }));
|
|
|
- };
|
|
|
- const setdelLoadingChange = (id: string, flag: boolean) => {
|
|
|
- setModules((prev) =>
|
|
|
- prev.map((module: any) => {
|
|
|
- if (module.id === id) {
|
|
|
- return { ...module, delLoading: flag }
|
|
|
- } else {
|
|
|
- return module
|
|
|
- }
|
|
|
- }));
|
|
|
- };
|
|
|
- // 恢复内容
|
|
|
- const handleRestore = (item: any) => {
|
|
|
- handleContentChange(item.id, item.sliceText);
|
|
|
- };
|
|
|
-
|
|
|
- // 新增模块
|
|
|
- const handleAddModule = (currentId?: string) => {
|
|
|
- let createdModule: ModuleItem | any = null;
|
|
|
- setModules((prev) => {
|
|
|
- const currentIndex = currentId
|
|
|
- ? prev.findIndex((module) => module.id === currentId)
|
|
|
- : prev.length - 1;
|
|
|
- const nextIndex = currentIndex >= 0 ? currentIndex + 1 : prev.length;
|
|
|
- createdModule = {
|
|
|
- id: `m${Date.now()}`,
|
|
|
- content: '### 新增切片',
|
|
|
- mdContent: '### 新增切片',
|
|
|
- };
|
|
|
- const nextModules = [...prev];
|
|
|
- nextModules.splice(nextIndex, 0, createdModule);
|
|
|
- return nextModules;
|
|
|
- });
|
|
|
- if (createdModule) {
|
|
|
- setActiveModule(createdModule.id, true);
|
|
|
- }
|
|
|
- };
|
|
|
- // 合并模块
|
|
|
- const handleMergeModule = (currentModule: any) => {
|
|
|
- if (!currentModule) return;
|
|
|
-
|
|
|
- // 如果没有选中任何模块,提示用户
|
|
|
- if (selectedModuleIds.length === 0) {
|
|
|
- message.warning('请先勾选要合并的模块');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // 获取所有需要合并的模块(包括当前模块),按照在modules数组中的顺序
|
|
|
- const modulesToMerge = modules.filter(m =>
|
|
|
- m.id === currentModule.id || selectedModuleIds.includes(m.id)
|
|
|
- );
|
|
|
-
|
|
|
- if (modulesToMerge.length <= 1) {
|
|
|
- message.warning('至少需要选择一个其他模块进行合并');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // 按照从上到下的顺序合并内容
|
|
|
- const mergedContent = modulesToMerge.map(m => m.content).join('\n');
|
|
|
-
|
|
|
- // 合并所有模块的 mediaList
|
|
|
- const allMediaLists = modulesToMerge
|
|
|
- .map(m => m.mediaList || [])
|
|
|
- .flat();
|
|
|
-
|
|
|
- // 去重 mediaList(根据 originText 或其他唯一标识)
|
|
|
- const uniqueMediaList = allMediaLists.filter((item, index, self) =>
|
|
|
- index === self.findIndex(t => t.originText === item.originText)
|
|
|
- );
|
|
|
-
|
|
|
- // 收集要删除的 sliceIds
|
|
|
- const idsToDelete = modulesToMerge
|
|
|
- .filter(m => m.id !== currentModule.id && m.sliceId)
|
|
|
- .map(m => m.sliceId!);
|
|
|
-
|
|
|
- // 更新 modules:保留当前模块并更新其内容,删除其他被合并的模块
|
|
|
- setModules((prev) => {
|
|
|
- const next = prev.filter(m =>
|
|
|
- m.id === currentModule.id || !selectedModuleIds.includes(m.id)
|
|
|
- );
|
|
|
-
|
|
|
- // 更新当前模块的内容
|
|
|
- return next.map(m => {
|
|
|
- if (m.id === currentModule.id) {
|
|
|
- return {
|
|
|
- ...m,
|
|
|
- content: mergedContent,
|
|
|
- mdContent: customRender(mergedContent, uniqueMediaList),
|
|
|
- mediaList: uniqueMediaList.length > 0 ? uniqueMediaList : undefined
|
|
|
- };
|
|
|
- }
|
|
|
- return m;
|
|
|
- });
|
|
|
- });
|
|
|
-
|
|
|
- // 添加到删除列表
|
|
|
- if (idsToDelete.length > 0) {
|
|
|
- setDelSliceIds((prev) => [...prev, ...idsToDelete]);
|
|
|
- }
|
|
|
-
|
|
|
- // 清空选中状态
|
|
|
- setSelectedModuleIds([]);
|
|
|
-
|
|
|
- message.success(`已合并 ${modulesToMerge.length} 个模块`);
|
|
|
- };
|
|
|
-
|
|
|
- // 切分模块
|
|
|
- const handleSplitModule = (module?: any) => {
|
|
|
- const target = module || modules.find((m) => m.id === activeId);
|
|
|
- if (!target) return;
|
|
|
-
|
|
|
- const text = (target.content || '').toString();
|
|
|
- const pos = Math.max(0, Math.min(cursorEndPosition || 0, text.length));
|
|
|
-
|
|
|
- // 如果位置在开头或结尾,则不进行切分
|
|
|
- if (pos === 0 || pos === text.length) {
|
|
|
- message.warning('当前位置不能切分内容');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- const firstPart = text.slice(0, pos);
|
|
|
- const secondPart = text.slice(pos);
|
|
|
-
|
|
|
- // 尝试保留 mediaList(如果有),这里简单复制到两个片段,确保渲染时可用
|
|
|
- const mediaList = target.mediaList ? [...target.mediaList] : undefined;
|
|
|
-
|
|
|
- const newModule = {
|
|
|
- id: `m${Date.now()}`,
|
|
|
- content: secondPart,
|
|
|
- mdContent: customRender(secondPart, mediaList || []),
|
|
|
- saveLoading: false,
|
|
|
- delLoading: false,
|
|
|
- mediaList: mediaList ? [...mediaList] : undefined,
|
|
|
- } as any;
|
|
|
-
|
|
|
- setModules((prev) => {
|
|
|
- const idx = prev.findIndex((m) => m.id === target.id);
|
|
|
- if (idx === -1) return prev;
|
|
|
- const next = [...prev];
|
|
|
- // 更新当前为第一段
|
|
|
- next[idx] = { ...next[idx], content: firstPart, mdContent: customRender(firstPart, mediaList || []) };
|
|
|
- // 在其后插入新模块(第二段)
|
|
|
- next.splice(idx + 1, 0, newModule);
|
|
|
- return next;
|
|
|
- });
|
|
|
-
|
|
|
- // 将焦点切到新创建的模块(编辑端)
|
|
|
- setActiveModule(newModule.id, true);
|
|
|
- // 重置光标位置
|
|
|
- setCursorEndPosition(0);
|
|
|
- };
|
|
|
- // 保存
|
|
|
- const handleSaveModule = async (item: any, index: number) => {
|
|
|
- if (!item.content || item.content.trim() === '') {
|
|
|
- return message.warning('切片内容不能为空');
|
|
|
- }
|
|
|
- const params = {
|
|
|
- ...item,
|
|
|
- sliceText: item.content,
|
|
|
- index: index + 1,
|
|
|
- }
|
|
|
- setSaveLoadingChange(item.id, true);
|
|
|
- try {
|
|
|
- if (item.sliceId) {
|
|
|
- const res: any = await apis.setupdateSliceInfo(params);
|
|
|
- if (res.code === 200) {
|
|
|
- message.success('修改成功');
|
|
|
- }
|
|
|
- }
|
|
|
- if (!item.sliceId) {
|
|
|
- params['documentId'] = detailDocument.documentId;
|
|
|
- params['knowledgeId'] = detailDocument.knowledgeId;
|
|
|
- const res: any = await apis.addSlice(params);
|
|
|
- if (res.code === 200) {
|
|
|
- message.success('保存成功');
|
|
|
- }
|
|
|
- }
|
|
|
- setSaveLoadingChange(item.id, false);
|
|
|
- } catch (error: any) {
|
|
|
- setSaveLoadingChange(item.id, false);
|
|
|
- }
|
|
|
- };
|
|
|
- // 删除模块
|
|
|
- const handleDeleteModule = async (item: any) => {
|
|
|
- if (item.sliceId) {
|
|
|
- setdelLoadingChange(item.id, true);
|
|
|
- const res = await apis.deleteTakaiSlice(item.sliceId, item.knowledgeId, item.documentId);
|
|
|
- setdelLoadingChange(item.id, false);
|
|
|
- if (res.code === 200) {
|
|
|
- setModules((prev) => {
|
|
|
- if (prev.length === 1) {
|
|
|
- message.warning('至少保留一个模块');
|
|
|
- return prev;
|
|
|
- }
|
|
|
- const filtered = prev.filter((module) => module.id !== item.id);
|
|
|
- if (activeId === item.id && filtered.length) {
|
|
|
- setActiveModule(filtered[0].id, true);
|
|
|
- }
|
|
|
- return filtered;
|
|
|
- });
|
|
|
- setDelSliceIds((prev) => {
|
|
|
- const delIds = [...prev];
|
|
|
- if (item && item.sliceId) {
|
|
|
- delIds.push(item.sliceId);
|
|
|
- }
|
|
|
- return delIds;
|
|
|
- })
|
|
|
- }
|
|
|
- } else {
|
|
|
- setModules((prev) => {
|
|
|
- if (prev.length === 1) {
|
|
|
- message.warning('至少保留一个模块');
|
|
|
- return prev;
|
|
|
- }
|
|
|
- const filtered = prev.filter((module) => module.id !== item.id);
|
|
|
- if (activeId === item.id && filtered.length) {
|
|
|
- setActiveModule(filtered[0].id, true);
|
|
|
- }
|
|
|
- return filtered;
|
|
|
- });
|
|
|
- setDelSliceIds((prev) => {
|
|
|
- const delIds = [...prev];
|
|
|
- if (item && item.sliceId) {
|
|
|
- delIds.push(item.sliceId);
|
|
|
- }
|
|
|
- return delIds;
|
|
|
- })
|
|
|
- }
|
|
|
- };
|
|
|
- // 切换模块选中状态
|
|
|
- const handleToggleModuleSelection = (moduleId: string) => {
|
|
|
- setSelectedModuleIds((prev) => {
|
|
|
- if (prev.includes(moduleId)) {
|
|
|
- return prev.filter(id => id !== moduleId);
|
|
|
- } else {
|
|
|
- return [...prev, moduleId];
|
|
|
- }
|
|
|
- });
|
|
|
- };
|
|
|
-
|
|
|
- // 处理markdown编辑器
|
|
|
- const [cursorEndPosition, setCursorEndPosition] = React.useState<number>(0);
|
|
|
- const [SliceDetail, setSliceDetail] = React.useState<any>(null);
|
|
|
- const [upload, setUpload] = React.useState<boolean>(false);
|
|
|
-
|
|
|
- const upDateButton = (module?: any) => {
|
|
|
- // 上传图片配置
|
|
|
- const uploadImageConfig: UploadProps = {
|
|
|
- name: 'file',
|
|
|
- action: `/api/resource/oss/upload`,
|
|
|
- method: 'POST',
|
|
|
- headers: getHeaders(),
|
|
|
- accept: ['.png', '.jpg', '.jpeg'].join(','),
|
|
|
- multiple: true,
|
|
|
- maxCount: 5,
|
|
|
- showUploadList: false,
|
|
|
- };
|
|
|
- return (
|
|
|
- <Upload
|
|
|
- {...uploadImageConfig}
|
|
|
- onChange={async (info) => {
|
|
|
- const file = info.file;
|
|
|
- // 上传中,设置模块 uploading 状态
|
|
|
- if (file.status === 'uploading' && module) {
|
|
|
- setModules((prev) => prev.map((m) => (m.id === module.id ? { ...m, uploading: true } : m)));
|
|
|
- }
|
|
|
- const insertToSliceText = (item: any) => {
|
|
|
- const slice_text = SliceDetail?.content || '';
|
|
|
- // 获取当前光标位置
|
|
|
- const position = cursorEndPosition;
|
|
|
-
|
|
|
- let newValue = '';
|
|
|
-
|
|
|
- if (!slice_text) {
|
|
|
- newValue = item.originText;
|
|
|
- } else {
|
|
|
- newValue = slice_text.slice(0, position) + item.originText + slice_text.slice(position);
|
|
|
- }
|
|
|
- const falgImages = SliceDetail.mediaList ? [...SliceDetail.mediaList, item] : [item];
|
|
|
- handleContentChange(SliceDetail.id, newValue, falgImages)
|
|
|
- // form.setFieldsValue({ slice_text: newValue });
|
|
|
- }
|
|
|
- if (file.status === 'done') {// 上传成功
|
|
|
- const { code, msg, data } = file.response;
|
|
|
- if (code === 200) {
|
|
|
- const res: any = await apis.uploadSliceImage(detailDocument, {
|
|
|
- ...data,
|
|
|
- name: data.fileName,
|
|
|
- });
|
|
|
- if (res.code === 200) {
|
|
|
- insertToSliceText(res.data)
|
|
|
- // 上传完成,清除 uploading 状态
|
|
|
- if (module) {
|
|
|
- setModules((prev) => prev.map((m) => (m.id === module.id ? { ...m, uploading: false } : m)));
|
|
|
- }
|
|
|
- message.success('上传成功');
|
|
|
- }
|
|
|
- } else {
|
|
|
- message.error(msg);
|
|
|
- }
|
|
|
- } else if (file.status === 'error') {// 上传失败
|
|
|
- // 上传失败,清除 uploading 状态
|
|
|
- if (module) {
|
|
|
- setModules((prev) => prev.map((m) => (m.id === module.id ? { ...m, uploading: false } : m)));
|
|
|
- }
|
|
|
- message.error('上传失败');
|
|
|
- }
|
|
|
- }}
|
|
|
- >
|
|
|
- <Tooltip title="解析图片">
|
|
|
- <Button size='small' style={{ border: 'none', padding: 0 }} loading={module?.uploading}>
|
|
|
- {/* 解析图片 */}
|
|
|
- <Image
|
|
|
- width={23}
|
|
|
- height={23}
|
|
|
- src={zJiexi}
|
|
|
- preview={false}
|
|
|
- className='cursor-pointer'
|
|
|
- onClick={() => handleRestore(module)}
|
|
|
- />
|
|
|
- </Button>
|
|
|
- </Tooltip>
|
|
|
- </Upload>
|
|
|
- )
|
|
|
- }
|
|
|
- // 渲染编辑模块
|
|
|
- const renderEditorModules = () => {
|
|
|
- const textAreaProps = {
|
|
|
- autoSize: { minRows: 4, maxRows: 20 },
|
|
|
- className:
|
|
|
- '!bg-white !border-none !shadow-none !p-0 !text-sm !leading-6 focus:!shadow-none',
|
|
|
- };
|
|
|
-
|
|
|
- return modules.map((module: any, index) => (
|
|
|
- <div
|
|
|
- key={module.id}
|
|
|
- data-id={module.id}
|
|
|
- className={`rounded-2xl mb-[10px] border border-transparent p-4 bg-white transition-all duration-200 relative
|
|
|
-
|
|
|
- ${activeId === module.id
|
|
|
- ? 'border-blue-300 ring-2 ring-blue-200'
|
|
|
- : 'hover:border-blue-200'
|
|
|
- }`}
|
|
|
- >
|
|
|
- {/* 左上角废弃*/}
|
|
|
- {module.revisionStatus === '0' && <div className="absolute top-[-0px] right-4 z-10">
|
|
|
- <Tooltip title='已废弃'>
|
|
|
- <Image width={16} height={16} src={rfq} preview={false} className='cursor-pointer ml-2' />
|
|
|
- </Tooltip>
|
|
|
- </div>}
|
|
|
- <div className={`relative w-full h-full ${module.revisionStatus === '0' ? 'pointer-events-none cursor-not-allowed' : ''}`}>
|
|
|
- {/* 右上角多选框 */}
|
|
|
- {module.revisionStatus !== '0' && <div className="absolute top-[-15px] right-1 z-10">
|
|
|
- <Checkbox
|
|
|
- checked={selectedModuleIds.includes(module.id)}
|
|
|
- onChange={() => handleToggleModuleSelection(module.id)}
|
|
|
- onClick={(e) => e.stopPropagation()}
|
|
|
- />
|
|
|
- </div>}
|
|
|
- <TextArea
|
|
|
- {...textAreaProps}
|
|
|
- value={module.content}
|
|
|
- onChange={(e) => handleContentChange(module.id, e.target.value)}
|
|
|
- onFocus={() => setActiveModule(module.id, true)}
|
|
|
- onBlur={(e: React.FocusEvent<HTMLTextAreaElement>) => {
|
|
|
- const ta = e.target as HTMLTextAreaElement;
|
|
|
- const start = ta.selectionStart;
|
|
|
- const end = ta.selectionEnd;
|
|
|
- setCursorEndPosition(end);
|
|
|
- setSliceDetail(module)
|
|
|
- // setCursor({ start, end });
|
|
|
- // console.log('blur cursor', start, end, ta.value);
|
|
|
- }}
|
|
|
- />
|
|
|
- {activeId === module.id && (
|
|
|
- <Space size={[8, 8]} wrap className="mt-3 w-full flex justify-end">
|
|
|
- {/* 恢复初始 */}
|
|
|
- {module.sliceId &&
|
|
|
- <Tooltip title="恢复初始内容">
|
|
|
- <Image
|
|
|
- width={23}
|
|
|
- height={23}
|
|
|
- src={zHuifu}
|
|
|
- preview={false}
|
|
|
- className='cursor-pointer'
|
|
|
- onClick={() => handleRestore(module)}
|
|
|
- />
|
|
|
- </Tooltip>
|
|
|
- }
|
|
|
- {upDateButton(module)}
|
|
|
- <Tooltip title="切分模块">
|
|
|
- <Button
|
|
|
- style={{ border: 'none', padding: 0 }}
|
|
|
- size="small"
|
|
|
- type="primary"
|
|
|
- ghost
|
|
|
- onClick={() => handleSplitModule(module)}
|
|
|
- >
|
|
|
- <Image src={zQiefen} width={23} height={23} preview={false} className='cursor-pointer' />
|
|
|
- </Button>
|
|
|
- </Tooltip>
|
|
|
- <Tooltip title="合并模块(请先勾选要合并的模块)">
|
|
|
- <Button
|
|
|
- style={{ border: 'none', padding: 0 }}
|
|
|
- size="small"
|
|
|
- type="primary"
|
|
|
- ghost
|
|
|
- onClick={() => handleMergeModule(module)}
|
|
|
- >
|
|
|
- <Image src={zHebing} width={23} height={23} preview={false} className='cursor-pointer' />
|
|
|
- </Button>
|
|
|
- </Tooltip>
|
|
|
- <Tooltip title="新增模块">
|
|
|
- <Button
|
|
|
- size="small"
|
|
|
- type="primary"
|
|
|
- style={{ border: 'none', padding: 0 }}
|
|
|
- ghost
|
|
|
- onClick={() => handleAddModule(module.id)}
|
|
|
- >
|
|
|
- <Image src={zXinzeng} width={23} height={23} preview={false} className='cursor-pointer' />
|
|
|
- </Button>
|
|
|
- </Tooltip>
|
|
|
- <Button
|
|
|
- size="small"
|
|
|
- loading={module.delLoading}
|
|
|
- danger
|
|
|
- onClick={() => handleDeleteModule(module)}
|
|
|
- >
|
|
|
- 删除
|
|
|
- </Button>
|
|
|
- <Button
|
|
|
- size="small"
|
|
|
- type="primary"
|
|
|
- loading={module.saveLoading}
|
|
|
- onClick={() => handleSaveModule(module, index)}
|
|
|
- >
|
|
|
- 保存
|
|
|
- </Button>
|
|
|
- <div className="absolute bottom-0 left-0 z-10">
|
|
|
- <Popconfirm
|
|
|
- placement="topLeft"
|
|
|
- showCancel={false}
|
|
|
- title={
|
|
|
- <div style={{ maxWidth: 360 }}>
|
|
|
- <div style={{ marginBottom: 8 }}>
|
|
|
- <div style={{ fontWeight: 600 }}>生成QA问答对</div>
|
|
|
- <div style={{ fontSize: 12, color: '#666' }}>
|
|
|
- {(() => {
|
|
|
- const trySources = [
|
|
|
- module?.qa,
|
|
|
- module?.mode?.qa,
|
|
|
- knowledgeDetail?.mode?.qa,
|
|
|
- detailDocument?.mode?.qa,
|
|
|
- module?.question,
|
|
|
- ];
|
|
|
- let qaRaw: any = null;
|
|
|
- for (const s of trySources) {
|
|
|
- if (s) {
|
|
|
- qaRaw = s;
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
- if (!qaRaw) return '暂无问答';
|
|
|
- let qaList: any[] = [];
|
|
|
- if (typeof qaRaw === 'string') {
|
|
|
- try {
|
|
|
- qaList = JSON.parse(qaRaw);
|
|
|
- } catch (e) {
|
|
|
- return qaRaw;
|
|
|
- }
|
|
|
- } else if (Array.isArray(qaRaw)) {
|
|
|
- qaList = qaRaw;
|
|
|
- } else if (qaRaw && typeof qaRaw === 'object') {
|
|
|
- qaList = qaRaw.qa || qaRaw.list || [];
|
|
|
- }
|
|
|
- if (!qaList || qaList.length === 0) return '暂无问答';
|
|
|
- return (
|
|
|
- <div>
|
|
|
- {qaList.slice(0, 5).map((item: any, idx: number) => (
|
|
|
- <div key={idx} style={{ marginBottom: 6 }}>
|
|
|
- <div style={{ fontWeight: 500 }}>{item.question}</div>
|
|
|
- <div style={{ fontSize: 12, color: '#444' }}>{item.answer}</div>
|
|
|
- </div>
|
|
|
- ))}
|
|
|
- {qaList.length > 5 && <div style={{ fontSize: 12, color: '#999' }}>...共{qaList.length}条</div>}
|
|
|
- </div>
|
|
|
- );
|
|
|
- })()}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div style={{ marginBottom: 8 }}>
|
|
|
- <div style={{ fontWeight: 600 }}>生成关联问题</div>
|
|
|
- <div style={{ fontSize: 12, color: '#666' }}>{module?.question || '暂无关联问题'}</div>
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <div style={{ fontWeight: 600 }}>生成摘要</div>
|
|
|
- <div style={{ fontSize: 12, color: '#666' }}>{module?.summary || '暂无摘要'}</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- }
|
|
|
- okText="关闭"
|
|
|
- onConfirm={() => { }}
|
|
|
- >
|
|
|
- <Image
|
|
|
- width={16}
|
|
|
- height={16}
|
|
|
- src={zTuozhan}
|
|
|
- preview={false}
|
|
|
- className='cursor-pointer'
|
|
|
- />
|
|
|
- </Popconfirm>
|
|
|
- {module.revisionStatus === '1' &&
|
|
|
- <Popconfirm
|
|
|
- placement="topLeft"
|
|
|
- showCancel={false}
|
|
|
- title={
|
|
|
- <div style={{ maxWidth: 500 }}>
|
|
|
- <div style={{ marginBottom: 8 }}>
|
|
|
- <div style={{ fontWeight: 600 }}>{module.documentName}</div>
|
|
|
- <div style={{ marginTop: 6, marginRight: 18 }}>
|
|
|
- <div style={{
|
|
|
- background: '#f5f7ff',
|
|
|
- padding: '10px',
|
|
|
- borderRadius: 6,
|
|
|
- border: '1px solid rgba(23,70,161,0.06)',
|
|
|
- fontSize: 13,
|
|
|
- color: '#222',
|
|
|
- lineHeight: '1.6',
|
|
|
- whiteSpace: 'pre-wrap'
|
|
|
- }}>
|
|
|
- {module.revisionSliceText}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'flex-end', marginTop: 10, alignItems: 'flex-end' }}>
|
|
|
- <div style={{ fontSize: 12, color: '#666' }}>修订人:{module?.revisionName}</div>
|
|
|
- <div style={{ fontSize: 12, color: '#666' }}>修订时间:{module?.revisionTime}</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- }
|
|
|
- okText="关闭"
|
|
|
- onConfirm={() => { }}
|
|
|
- >
|
|
|
- <Image width={16} height={16} src={tubing} preview={false} className='cursor-pointer ml-2' />
|
|
|
- </Popconfirm>
|
|
|
- }
|
|
|
- </div>
|
|
|
- </Space>
|
|
|
- )}
|
|
|
- </div>
|
|
|
-
|
|
|
- </div>
|
|
|
- ));
|
|
|
- };
|
|
|
- // 渲染编辑模块-MD
|
|
|
- const renderMdEditorModules = () => {
|
|
|
- const textAreaProps = {
|
|
|
- autoSize: { minRows: 4, maxRows: 25 },
|
|
|
- className:
|
|
|
- '!bg-white !border-none !shadow-none !p-0 !text-sm !leading-6 focus:!shadow-none',
|
|
|
- };
|
|
|
- return (
|
|
|
- <div
|
|
|
- className={`rounded-2xl mb-[10px] border border-transparent bg-white p-2 transition-all duration-200 relative
|
|
|
- border-blue-300 ring-2 ring-blue-200`}
|
|
|
- >
|
|
|
- <TextArea
|
|
|
- {...textAreaProps}
|
|
|
- value={oneModules}
|
|
|
- onChange={(e) => { setOneModules(e.target.value) }}
|
|
|
- />
|
|
|
- </div>
|
|
|
- )
|
|
|
- }
|
|
|
- // 渲染预览模块
|
|
|
- const renderPreviewDocument = () => {
|
|
|
- return (
|
|
|
- <div className="overflow-hidden rounded-2xl bg-white w-full">
|
|
|
- <style>{`
|
|
|
- .markdown-preview img,table {
|
|
|
- max-width: 100%;
|
|
|
- width: 100% !important;
|
|
|
- height: auto;
|
|
|
- display: block;
|
|
|
- }
|
|
|
- `}</style>
|
|
|
- {modules.map((module, index) => (
|
|
|
- <div
|
|
|
- key={module.id}
|
|
|
- data-id={module.id}
|
|
|
- className={`relative px-3 py-2 text-sm leading-7 transition-all duration-200 cursor-pointer ${index !== modules.length - 1 ? 'border-b border-gray-100' : ''
|
|
|
- } ${activeId === module.id
|
|
|
- ? 'bg-blue-50 border-l-blue-500 shadow-sm scale-[1]'
|
|
|
- : 'hover:bg-gray-50'
|
|
|
- }`}
|
|
|
- onClick={() => setActiveModule(module.id, false)}
|
|
|
- >
|
|
|
- <Typography
|
|
|
- className="text-sm leading-7 text-gray-800 markdown-preview"
|
|
|
- dangerouslySetInnerHTML={{
|
|
|
- __html: marked.render(module.mdContent),
|
|
|
- }}
|
|
|
- />
|
|
|
- </div>
|
|
|
- ))}
|
|
|
- </div>
|
|
|
- );
|
|
|
- };
|
|
|
- //渲染预览模块-Md
|
|
|
- const renderMdPreviewDocument = () => {
|
|
|
- return (
|
|
|
- <div className="overflow-hidden rounded-2xl bg-white w-full">
|
|
|
- <style>{`
|
|
|
- .markdown-preview img,table {
|
|
|
- max-width: 100%;
|
|
|
- width: 100% !important;
|
|
|
- height: auto;
|
|
|
- display: block;
|
|
|
- }
|
|
|
- .markdown-preview table{
|
|
|
- border-collapse: collapse;
|
|
|
- width: 100%;
|
|
|
- }
|
|
|
- .markdown-preview th, .markdown-preview td{
|
|
|
- border: 1px solid #ddd;
|
|
|
- }
|
|
|
- `}</style>
|
|
|
- <div
|
|
|
- className={`relative px-3 py-2 text-sm leading-7 transition-all duration-200 cursor-pointer border-b border-gray-100 bg-blue-50 border-l-blue-500 shadow-sm`}
|
|
|
- >
|
|
|
- <Typography
|
|
|
- className="text-sm leading-7 text-gray-800 markdown-preview"
|
|
|
- dangerouslySetInnerHTML={{
|
|
|
- __html: marked.render(oneModules),
|
|
|
- }}
|
|
|
- />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- );
|
|
|
- };
|
|
|
-
|
|
|
-
|
|
|
- const handleSearch = () => {
|
|
|
- setHasMore(true);
|
|
|
- loadKnowledgeList(1, keyword);
|
|
|
- };
|
|
|
- const [qaChecked, setQaChecked] = useState(false); // 生成QA问答对
|
|
|
- const [relatedQuestionsEnabled, setRelatedQuestionsEnabled] = useState(false); // 生成关联问题
|
|
|
- const [summaryGenerationEnabled, setSummaryGenerationEnabled] = useState(false); // 生成摘要
|
|
|
- const [parentGenerationEnabled, setParentGenerationEnabled] = useState(false); // 开启父子切片召回
|
|
|
- const [allLoding, setAllLoding] = useState(false);
|
|
|
- const mdUpdateSliceList = async () => {
|
|
|
- const params = {
|
|
|
- documentId: selectedDoc?.documentId,
|
|
|
- markDownText: encodeURIComponent(oneModules),
|
|
|
- knowledgeId: detailDocument?.knowledgeId,
|
|
|
- };
|
|
|
- try {
|
|
|
- message.info({
|
|
|
- duration: 2,
|
|
|
- content: '正在保存Md切片信息...',
|
|
|
- });
|
|
|
- setTimeout(() => {
|
|
|
- onCancel();
|
|
|
- }, 1500)
|
|
|
- const res: any = await apis.editMarkDownFileApi(params);
|
|
|
- if (res.code === 200) {
|
|
|
- // message.success('保存成功');
|
|
|
- const timestampMs = Date.now();
|
|
|
- onSetStarts(timestampMs)
|
|
|
- loadKnowledgeList(1, '');
|
|
|
- onCancel(1);
|
|
|
- } else {
|
|
|
- message.error(res.msg || '保存失败');
|
|
|
- }
|
|
|
- } catch (error: any) {
|
|
|
-
|
|
|
- } finally {
|
|
|
- setAllLoding(false);
|
|
|
- }
|
|
|
- }
|
|
|
- const updateSliceInfoList = async () => {
|
|
|
- // 当保存时把当前文档状态设为 '4'(加载中),成功后设为 '5'
|
|
|
- const targetId = selectedDoc?.documentId || detailDocument?.documentId;
|
|
|
- // 记录原始状态以便错误时恢复
|
|
|
- let prevStatus: any = null;
|
|
|
- if (targetId) {
|
|
|
- setDocuments((prev) => {
|
|
|
- return prev.map((d) => {
|
|
|
- if (d.documentId === targetId) {
|
|
|
- prevStatus = d.status;
|
|
|
- return { ...d, status: '4' };
|
|
|
- }
|
|
|
- return d;
|
|
|
- });
|
|
|
- });
|
|
|
- // 同步更新 selectedDoc 的状态(如果当前选中)
|
|
|
- if (selectedDoc && selectedDoc.documentId === targetId) {
|
|
|
- setSelectedDoc({ ...selectedDoc, status: '4' });
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- setAllLoding(true);
|
|
|
- if (tabKey === 'md') {
|
|
|
- // 仅在切片模式下执行保存逻辑
|
|
|
- mdUpdateSliceList();
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- const params = {
|
|
|
- documentId: selectedDoc?.documentId,
|
|
|
- knowledgeId: detailDocument?.knowledgeId,
|
|
|
- sliceList: modules.map((item, index) => ({
|
|
|
- ...item,
|
|
|
- sliceText: item.content,
|
|
|
- index: index + 1,
|
|
|
- })),
|
|
|
- delSliceIds,
|
|
|
- qaChecked: qaChecked,
|
|
|
- relatedQuestionsEnabled: relatedQuestionsEnabled,
|
|
|
- summaryGenerationEnabled: summaryGenerationEnabled,
|
|
|
- parentGenerationEnabled: parentGenerationEnabled,
|
|
|
- };
|
|
|
- try {
|
|
|
- message.info({
|
|
|
- duration: 2,
|
|
|
- content: '正在保存切片信息...',
|
|
|
- });
|
|
|
- setTimeout(() => {
|
|
|
- onCancel();
|
|
|
- }, 1000)
|
|
|
- const res: any = await apis.updateSliceInfoList(params);
|
|
|
- setAllLoding(false);
|
|
|
- if (res.code === 200) {
|
|
|
- const timestampMs = Date.now();
|
|
|
- onSetStarts(timestampMs)
|
|
|
- // 更新状态为 '5' 表示已完成
|
|
|
- loadKnowledgeList(1, '');
|
|
|
- if (targetId) {
|
|
|
- setDocuments((prev) => prev.map((d) => (d.documentId === targetId ? { ...d, status: '5' } : d)));
|
|
|
- if (selectedDoc && selectedDoc.documentId === targetId) {
|
|
|
- setSelectedDoc({ ...selectedDoc, status: '5' });
|
|
|
- }
|
|
|
- }
|
|
|
- // message.success('保存成功');
|
|
|
- const resdata = res.data;
|
|
|
- onCancel(1);
|
|
|
- if (selectedRefDoc.current?.documentId === resdata?.documentId) {
|
|
|
- // console.log('selectedDoc--targetId--',selectedRefDoc.current?.documentId,'--resdata?.documentId',resdata?.documentId)
|
|
|
- // onCancel();
|
|
|
- }
|
|
|
- } else {
|
|
|
- // 恢复原始状态
|
|
|
- if (targetId) {
|
|
|
- setDocuments((prev) => prev.map((d) => (d.documentId === targetId ? { ...d, status: prevStatus } : d)));
|
|
|
- if (selectedDoc && selectedDoc.documentId === targetId) {
|
|
|
- setSelectedDoc({ ...selectedDoc, status: prevStatus });
|
|
|
- }
|
|
|
- }
|
|
|
- message.error(res.msg || '保存失败');
|
|
|
- }
|
|
|
- } catch (error: any) {
|
|
|
- // 恢复原始状态
|
|
|
- if (targetId) {
|
|
|
- setDocuments((prev) => prev.map((d) => (d.documentId === targetId ? { ...d, status: prevStatus } : d)));
|
|
|
- if (selectedDoc && selectedDoc.documentId === targetId) {
|
|
|
- setSelectedDoc({ ...selectedDoc, status: prevStatus });
|
|
|
- }
|
|
|
- }
|
|
|
- setAllLoding(false);
|
|
|
- }
|
|
|
-
|
|
|
- }
|
|
|
- const getOldSliceListByDocumentId = async () => {
|
|
|
- const res: any = await apis.getOldSliceListByDocumentId(selectedDoc?.documentId)
|
|
|
- if (res.code === 200 && res.data) {
|
|
|
- sliceDataProcess(res.data)
|
|
|
- }
|
|
|
- }
|
|
|
- const [tabKey, setTabKey] = useState<string>('slices');
|
|
|
- // 批量删除
|
|
|
- const onAllDelete = () => {
|
|
|
- Modal.confirm({
|
|
|
- title: '将要删除多条选中的切片,是否确认',
|
|
|
- okText: '确认',
|
|
|
- cancelText: '取消',
|
|
|
- onOk: async () => {
|
|
|
- const res: any = await apis.deleteSliceListApi(selectedDoc?.knowledgeId, selectedDoc?.documentId, {
|
|
|
- sliceIds: selectedModuleIds,
|
|
|
- });
|
|
|
- if (res.code === 200) {
|
|
|
- message.success('已删除所选切片');
|
|
|
- setSelectedModuleIds([]);
|
|
|
- setModules((pre) => {
|
|
|
- return pre.filter((m: any) => !selectedModuleIds.includes(m.id));
|
|
|
- });
|
|
|
- }
|
|
|
- },
|
|
|
- });
|
|
|
- }
|
|
|
- // 如果是有Md编辑器推出的时候加个提示
|
|
|
- const onMdCancel = () => {
|
|
|
- if (selectedDoc?.standardClassification !== '2') {
|
|
|
- Modal.confirm({
|
|
|
- title: '提示',
|
|
|
- content: '退出后,当前未保存的编辑行为都将失效,是否确认退出',
|
|
|
- okText: '确认',
|
|
|
- okType: 'danger',
|
|
|
- cancelText: '取消',
|
|
|
- onOk: async () => {
|
|
|
- onCancel();
|
|
|
- }
|
|
|
- });
|
|
|
- } else {
|
|
|
- onCancel()
|
|
|
- }
|
|
|
- }
|
|
|
- const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(false); // 控制预览区域是否收起
|
|
|
-
|
|
|
- return (
|
|
|
- <>
|
|
|
- <Modal
|
|
|
- title="知识文档处理"
|
|
|
- open={open}
|
|
|
- onCancel={onMdCancel}
|
|
|
- footer={
|
|
|
- <div className="flex justify-end gap-3 items-center">
|
|
|
- {tabKey === 'slices' && <>
|
|
|
- <p className='font-bold'>拓展功能:</p>
|
|
|
- <Checkbox checked={qaChecked} onChange={(e) => setQaChecked(e.target.checked)}>生成QA问答对</Checkbox>
|
|
|
- <Checkbox checked={relatedQuestionsEnabled} onChange={(e) => setRelatedQuestionsEnabled(e.target.checked)}>生成关联问题</Checkbox>
|
|
|
- <Checkbox checked={summaryGenerationEnabled} onChange={(e) => setSummaryGenerationEnabled(e.target.checked)}>生成摘要</Checkbox>
|
|
|
- {customSeparator === '3' && <Checkbox checked={parentGenerationEnabled} onChange={(e) => setParentGenerationEnabled(e.target.checked)}>开启父子切片召回</Checkbox>}
|
|
|
- </>}
|
|
|
- <Button onClick={onMdCancel}>取消</Button>
|
|
|
- {<Button type="primary" loading={selectedDoc?.status === '4'} onClick={() => updateSliceInfoList()}>
|
|
|
- 保存
|
|
|
- </Button>}
|
|
|
- </div>
|
|
|
- }
|
|
|
- width="95%"
|
|
|
- maskClosable={false}
|
|
|
- style={{ top: 24 }}
|
|
|
- className="md-modal [&_.ant-modal-body]:p-0"
|
|
|
- >
|
|
|
- <div className="flex h-[85vh] gap-4 bg-gray-50 p-4">
|
|
|
- {/* 文档列表 */}
|
|
|
- <div
|
|
|
- className={`flex flex-col rounded-2xl bg-white shadow-md transition-all duration-0 ${isListCollapsed ? 'w-14' : 'w-72'
|
|
|
- }`}
|
|
|
- >
|
|
|
- <div
|
|
|
- className={`flex items-center border-b border-gray-200 ${isListCollapsed ? 'justify-center px-0 py-4' : 'justify-between px-4 py-3'
|
|
|
- }`}
|
|
|
- >
|
|
|
- {!isListCollapsed && (
|
|
|
- <Tooltip title={knowledgeDetail ? knowledgeDetail.name : '文档列表'}>
|
|
|
- <Typography.Title level={5} className="mb-0! text-base font-semibold mt-0 overflow-hidden text-ellipsis whitespace-nowrap max-w-[400px]">
|
|
|
- {knowledgeDetail ? knowledgeDetail.name : '文档列表'}
|
|
|
- </Typography.Title>
|
|
|
- </Tooltip>
|
|
|
- )}
|
|
|
- <Button
|
|
|
- type="text"
|
|
|
- size="small"
|
|
|
- icon={isListCollapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
|
|
- onClick={() => setIsListCollapsed((prev) => !prev)}
|
|
|
- />
|
|
|
- </div>
|
|
|
- {isListCollapsed ? (
|
|
|
- <div className="flex flex-1 items-center justify-center text-[11px] tracking-[0.4em] text-gray-400 [writing-mode:vertical-rl]">
|
|
|
- 文档列表
|
|
|
- </div>
|
|
|
- ) : (
|
|
|
- <>
|
|
|
- <div className="px-4 pt-3">
|
|
|
- <Space.Compact className="w-full" size="small">
|
|
|
- <Input
|
|
|
- placeholder="请输入文档名称"
|
|
|
- size='middle'
|
|
|
- value={keyword}
|
|
|
- onChange={(e) => {
|
|
|
- setKeyword(e.target.value);
|
|
|
- }}
|
|
|
- onPressEnter={handleSearch}
|
|
|
- />
|
|
|
- <Button
|
|
|
- type="primary"
|
|
|
- size="middle"
|
|
|
- icon={<SearchOutlined />}
|
|
|
- onClick={handleSearch}
|
|
|
- />
|
|
|
- </Space.Compact>
|
|
|
- </div>
|
|
|
- <div
|
|
|
- className="flex-1 space-y-2 overflow-y-auto px-4 pb-4 pt-3"
|
|
|
- ref={listContainerRef}
|
|
|
- >
|
|
|
- {documents.map((doc) => (
|
|
|
- <div
|
|
|
- key={doc.documentId}
|
|
|
- className={`rounded-lg border border-transparent bg-gray-50 p-2.5 text-sm transition-all cursor-pointer hover:border-blue-300 hover:bg-blue-50 ${selectedDoc?.documentId === doc.documentId
|
|
|
- ? 'border-blue-400 ring-1 ring-blue-300'
|
|
|
- : ''
|
|
|
- }`}
|
|
|
- onClick={() => { onExportPdfStream(doc); }}
|
|
|
- >
|
|
|
- <div className="font-medium text-gray-900 text-xs flex items-center">
|
|
|
- <FileTextOutlined style={{ color: selectedDoc?.documentId === doc.documentId ? '#1677ff' : '' }} />
|
|
|
- <div className="ml-2 flex-1 min-w-0 truncate" title={doc.name}>{doc.name}</div>
|
|
|
- {doc?.status === '4' && (
|
|
|
- <div className="ml-2 flex-none">
|
|
|
- <Spin size="small" />
|
|
|
- </div>
|
|
|
- )}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- ))}
|
|
|
- {listLoading && (
|
|
|
- <div className="flex items-center justify-center gap-2 py-4 text-xs text-gray-500">
|
|
|
- <Spin size="small" /> <span>加载中...</span>
|
|
|
- </div>
|
|
|
- )}
|
|
|
- {!listLoading && documents.length === 0 && (
|
|
|
- <div className="py-8">
|
|
|
- <Empty description="暂无文档" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
|
|
- </div>
|
|
|
- )}
|
|
|
- {!hasMore && documents.length > 0 && (
|
|
|
- <div className="py-2 text-center text-xs text-gray-400">没有更多了</div>
|
|
|
- )}
|
|
|
- </div>
|
|
|
- </>
|
|
|
- )}
|
|
|
- </div>
|
|
|
-
|
|
|
- {/* PDF预览 isPDfCollapsed */}
|
|
|
- {selectedDoc?.suffix !== 'md' && <div className={`flex flex-col rounded-2xl bg-white shadow-md transition-all duration-0 ${isPDfCollapsed ? 'w-14' : 'flex-1'}`}>
|
|
|
- <div className="px-5 pt-4 pb-2 flex items-center justify-between">
|
|
|
- {!isPDfCollapsed && <Tooltip title={selectedDoc ? selectedDoc.name : 'PDF 预览'}>
|
|
|
- <Typography.Title level={5} className="mb-0! text-base font-semibold mt-0 overflow-hidden text-ellipsis whitespace-nowrap max-w-[300px]">
|
|
|
- {selectedDoc ? selectedDoc.name : 'PDF 预览'}
|
|
|
- </Typography.Title>
|
|
|
- </Tooltip>}
|
|
|
- <p>
|
|
|
- {!isPDfCollapsed && (
|
|
|
- <Button
|
|
|
- type="text"
|
|
|
- size="small"
|
|
|
- icon={<FullscreenOutlined />}
|
|
|
- onClick={() => setPdfFullVisible(true)}
|
|
|
- />
|
|
|
- )}
|
|
|
- <Button
|
|
|
- type="text"
|
|
|
- size="small"
|
|
|
- icon={isPDfCollapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
|
|
- onClick={() => setIsPDfCollapsed((prev) => !prev)}
|
|
|
- />
|
|
|
- </p>
|
|
|
- </div>
|
|
|
- {isPDfCollapsed ? <div className="flex flex-1 items-center justify-center text-[11px] tracking-[0.4em] text-gray-400 [writing-mode:vertical-rl]">
|
|
|
- PDF列表
|
|
|
- </div> : <div className="flex flex-1 overflow-hidden pb-5 pt-3">
|
|
|
- <div ref={pdfScrollRef} className="h-full w-full overflow-y-auto">
|
|
|
- <div className="h-full min-h-[240px]">
|
|
|
- <Spin tip="pdf加载中..." spinning={pdfLoading} wrapperClassName="h-full flex justify-center">
|
|
|
- <div className="h-full w-full">
|
|
|
- {renderPdfPreview()}
|
|
|
- </div>
|
|
|
- </Spin>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>}
|
|
|
- </div>}
|
|
|
-
|
|
|
- {/* 智能切片 */}
|
|
|
- <div className="flex flex-1 flex-col rounded-2xl bg-white shadow-md">
|
|
|
- <div className="flex justify-between px-5 pb-2 pt-4 md_center">
|
|
|
-
|
|
|
- <Tabs
|
|
|
- activeKey={tabKey}
|
|
|
- onChange={(key) => setTabKey(key as 'slices' | 'md')}
|
|
|
- size="small"
|
|
|
- items={selectedDoc?.standardClassification === '2' || selectedDoc?.customSeparator === '0' || selectedDoc?.customSeparator === '1' ? [
|
|
|
- { key: 'slices', label: '智能切片' },
|
|
|
-
|
|
|
- ] : [{ key: 'slices', label: '智能切片' }, {
|
|
|
- key: 'md', label:
|
|
|
- <div>Md编辑器 <Tooltip title="markdown编辑仅支持最小段落切片(标准规范)和按标题段落切片"> <QuestionCircleOutlined /> </Tooltip> </div>
|
|
|
- },]}
|
|
|
- />
|
|
|
- <div>
|
|
|
- {tabKey === 'slices' && <Button size="small" disabled={!activeId} onClick={() => {
|
|
|
- Modal.confirm({
|
|
|
- title: '将放弃对切片的所有修改,恢复到初始状态,是否确认',
|
|
|
- okText: '确定',
|
|
|
- cancelText: '取消',
|
|
|
- onOk: () => {
|
|
|
- getOldSliceListByDocumentId();
|
|
|
- },
|
|
|
- })
|
|
|
- }}>
|
|
|
- 恢复全部
|
|
|
- </Button>}
|
|
|
- {tabKey === 'slices' && <Button size="small" disabled={!selectedModuleIds.length} className='ml-2'
|
|
|
- icon={<DeleteOutlined style={{ color: selectedModuleIds.length ? '#eb2f96' : '' }} />}
|
|
|
- onClick={() => {
|
|
|
- onAllDelete();
|
|
|
- }}>
|
|
|
- </Button>}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- {<div
|
|
|
- className="flex-1 space-y-4 overflow-y-auto px-3 pb-5 pt-3"
|
|
|
- ref={editorContainerRef}
|
|
|
- >
|
|
|
- <div className="h-full min-h-[240px] w-full">
|
|
|
- <Spin tip="切片加载中..." spinning={sliceLoading} wrapperClassName="h-full w-full flex justify-center">
|
|
|
- <div className="h-full w-full">
|
|
|
- {tabKey === 'slices' && renderEditorModules()}
|
|
|
- {tabKey === 'md' && renderMdEditorModules()}
|
|
|
- </div>
|
|
|
- </Spin>
|
|
|
- </div>
|
|
|
- </div>}
|
|
|
- </div>
|
|
|
-
|
|
|
- {/* 文档预览 */}
|
|
|
- {
|
|
|
- <div className={`flex flex-col rounded-2xl bg-white shadow-md transition-all duration-0 ${isPreviewCollapsed ? 'w-14' : 'flex-1'}`}>
|
|
|
- <div className={`flex items-center border-b border-gray-200 ${isPreviewCollapsed ? 'justify-center px-0 py-4' : 'justify-between px-4 py-3'
|
|
|
- }`} >
|
|
|
- {!isPreviewCollapsed && <Typography.Title level={5} className="mb-0! text-base font-semibold mt-0">
|
|
|
- 文档预览
|
|
|
- </Typography.Title>}
|
|
|
- <Button
|
|
|
- type="text"
|
|
|
- size="small"
|
|
|
- icon={<MenuUnfoldOutlined />}
|
|
|
- onClick={() => setIsPreviewCollapsed((prev) => !prev)}
|
|
|
- />
|
|
|
- </div>
|
|
|
- {isPreviewCollapsed ? <div className="flex flex-col items-center justify-center rounded-2xl bg-white shadow-md w-14 h-full">
|
|
|
- <div className="text-[11px] tracking-[0.4em] text-gray-400 [writing-mode:vertical-rl] mt-4">
|
|
|
- 文档预览
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- :
|
|
|
- <div
|
|
|
- className="flex-1 overflow-y-auto px-3 pb-5 pt-3"
|
|
|
- ref={previewContainerRef}
|
|
|
- >
|
|
|
- <div className="h-full min-h-[240px]">
|
|
|
- <Spin tip="预览文档加载中..." spinning={sliceLoading} wrapperClassName="h-full flex justify-center">
|
|
|
- <div className="h-full w-full">
|
|
|
- {tabKey === 'slices' && renderPreviewDocument()}
|
|
|
- {tabKey === 'md' && renderMdPreviewDocument()}
|
|
|
- </div>
|
|
|
- </Spin>
|
|
|
- </div>
|
|
|
- </div>}
|
|
|
- </div>}
|
|
|
- </div>
|
|
|
- </Modal>
|
|
|
- {/* 全屏 PDF 预览(改为左侧 Drawer,宽度 40%) */}
|
|
|
- <Drawer
|
|
|
- title={selectedDoc ? selectedDoc.name : 'PDF 预览'}
|
|
|
- open={pdfFullVisible}
|
|
|
- onClose={() => setPdfFullVisible(false)}
|
|
|
- placement="left"
|
|
|
- width="55%"
|
|
|
- footer={
|
|
|
- <div className="flex items-center gap-2">
|
|
|
- <Button size="small" onClick={() => setPdfFullScale((s) => Math.max(0.5, +(s - 0.25).toFixed(2)))}>-</Button>
|
|
|
- <div className="text-sm">{Math.round(pdfFullScale * 100)}%</div>
|
|
|
- <Button size="small" onClick={() => setPdfFullScale((s) => Math.min(3, +(s + 0.25).toFixed(2)))}>+</Button>
|
|
|
- <Button size="small" onClick={() => { setPdfFullScale(1.5); setPdfFullVisible(false); }}>关闭</Button>
|
|
|
- </div>
|
|
|
- }
|
|
|
- >
|
|
|
- <div style={{ height: '100%', overflow: 'auto' }}>
|
|
|
- {pdfBuffer ? (
|
|
|
- <Document file={pdfBuffer} onLoadSuccess={({ numPages }) => setNumPages(numPages)}>
|
|
|
- {Array.from(new Array(numPages), (_, i) => (
|
|
|
- <div key={i} style={{ display: 'flex', justifyContent: 'center', marginBottom: 12 }}>
|
|
|
- <Page pageNumber={i + 1} scale={pdfFullScale} renderAnnotationLayer={false} renderTextLayer={true} />
|
|
|
- </div>
|
|
|
- ))}
|
|
|
- </Document>
|
|
|
- ) : (
|
|
|
- <div className="flex h-full items-center justify-center">暂无 PDF</div>
|
|
|
- )}
|
|
|
- </div>
|
|
|
- </Drawer>
|
|
|
- </>
|
|
|
- );
|
|
|
-};
|
|
|
-
|
|
|
-export default observer(MdModal);
|