|
|
@@ -0,0 +1,1573 @@
|
|
|
+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 { observer } from 'mobx-react';
|
|
|
+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);
|