antd 使用记录

226 阅读2分钟

Modal中使用表单

Modal.confirm 中 使用 Form 表单

Tree 不展示浏览器title属性

通过titleRender属性,自定义标题,在子组件内返回元素最外层设置title=""

// 代码示意
<Tree
  titleRender={(nodeData) => (<TreeTitle nodeData={nodeData} />)}
/>

const TreeTitle = ({ nodeData }) => <div title="">{nodeData.title}</div>

Form表单使用JSON Editor

// JSONEditor.tsx
import { useEffect, useRef } from 'react';
import { JSONEditor } from 'vanilla-jsoneditor';

const SvelteJSONEditor = (props) => {
  const refContainer = useRef(null);
  const refEditor = useRef(null);

  useEffect(() => {
    // create editor
    refEditor.current = new JSONEditor({
      target: refContainer.current,
      props: {
        content: props.value,
      },
    });

    return () => {
      // destroy editor
      if (refEditor.current) {
        refEditor.current.destroy();
        refEditor.current = null;
      }
    };
  }, []);

  // update props
  useEffect(() => {
    if (refEditor.current) {
      refEditor.current.updateProps({ ...props });
    }
  }, [props]);

  return <div className="vanilla-jsoneditor-react" ref={refContainer}></div>;
};

export default SvelteJSONEditor;

// form中配置
<Form
  labelCol={{ span: 4 }}
  initialValues={{
    settings: settings ? { json: settings, text: undefined } : undefined,
  }}
  form={dashBoardForm}
>
  <Form.Item
    label="前端配置项"
    name="settings"
    rules={[{ required: true, message: '请输入配置项' }]}
  >
    <JSONEditor />
  </Form.Item>
</Form>

// 取值
const { settings } = await dashBoardForm.validateFields();
// 统一转换为JSON格式
const value = settings?.text ? JSON.parse(settings?.text) : settings.json;

Upload导入表格并解析表头

import * as XLSX from 'xlsx';

const onUploadChange: UploadProps['onChange'] = ({ file }) => {
  const { name, status } = file;
  if (status === 'error') {
    message.error("上传失败!");
  }
  if (status === 'done') {
    const fileReader = new FileReader();
    fileReader.onload = () => {
      const workbook = XLSX.read(fileReader.result, { type: 'binary' });
      const firstSheetName = workbook.SheetNames[0];
      const worksheet = workbook.Sheets[firstSheetName];
      const dataSource: string[][] = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
      setMatchFields(dataSource?.[0] || []);
    };
    fileReader.readAsArrayBuffer(file.originFileObj);
  }
};

<Upload 
  accept=".xls,.xlsx" 
  action="/app/platform/resource/attachments" 
  onChange={onUploadChange}
>
  <Button icon={<UploadOutlined />}>上传文件</Button>
</Upload>

前端处理表格文件并获取表头

import { ErrorCode, useDropzone } from 'react-dropzone';
const acceptUploadTypes = ['.xls', '.xlsx'];

const { acceptedFiles, getRootProps, getInputProps } = useDropzone({
  multiple: false,
  accept: { 'application/octet-stream': acceptUploadTypes },
  validator(file) {
    const isAcceptFile = acceptUploadTypes.some((type) => file?.name?.endsWith(type));

    return isAcceptFile
      ? null
      : {
          message: `仅支持上传 ${acceptUploadTypes.join('、')} 等类型文件`,
          code: ErrorCode.FileInvalidType,
        };
  },
  onDropRejected([{ errors }]) {
    message.error(errors[0].message);
  },
  onDrop: async (files) => {
    files.forEach((file) => {
      if (file) {
        const reader = new FileReader();
        reader.onload = () => {
          if (reader.result) {
            const workbook = XLSX.read(reader.result, { type: 'binary' });
            const firstSheetName = workbook.SheetNames[0];
            const worksheet = workbook.Sheets[firstSheetName];
            const dataSource: string[][] = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
            setMatchFields(dataSource?.[0] || []);
          }
        };
        reader.readAsArrayBuffer(file);
      }
    });
  },
});

<div {...getRootProps()}>
  <input {...getInputProps()} />
  <Button icon={<UploadOutlined />}>上传文件</Button>
</div>
{!!acceptedFiles.length && (
  <Space>
    <PaperClipOutlined />
    {acceptedFiles.map((i) => (
      <Typography.Paragraph type="success" key={i.name}>
        {i.name}
      </Typography.Paragraph>
    ))}
  </Space>
)}

Upload自定义文件上传并根据上传进度提示

// 上传物料表格文件
export const uploadFile = (warehouseId: number, data, onUploadProgress) =>
  post('/xx', {
    data,
    headers: { 'content-type': 'multipart/form-data' },
    onUploadProgress,
    params: { warehouseId },
  });

// 根据progressTimes判断是否需要提示
const onFileUpload: UploadProps["customRequest"] = async (options) => {
  const { onSuccess, onError, file } = options;
  const fmData = new FormData();
  fmData.append("file", file);
  try {
    const res = await api.inventory.uploadFile(storeId, fmData, (event) => {
      if (event.progress !== 1 && progressTimes < maxProgressTimes) {
        uploadRef.current = setTimeout(() => {
          setProgressTimes((times) => times + 1);
        }, 300);
      }
    });
    message.success("上传成功");
    setProgressTimes(0);
    onSuccess?.();
  } catch (error) {
    setProgressTimes(0);
    onError?.(error);
  }
};

<Upload customRequest={onFileUpload} accept=".xls,.xlsx" maxCount={1}>
  <Button icon={<UploadOutlined />}>
    上传文件
  </Button>
</Upload>

Upload上传PDF文件获取文件页数

import { PDFDocument } from 'pdf-lib';

const arrayBufferToString = (buffer) => {
  // 创建一个Uint8Array来操作ArrayBuffer
  const uintArray = new Uint8Array(buffer);

  // 创建一个TextDecoder对象
  const decoder = new TextDecoder();

  // 使用TextDecoder的decode方法将Uint8Array转为字符串
  const result = decoder.decode(uintArray);

  return result;
};


return new Promise((resolve, reject) => {
  const reader = new FileReader();
  reader.readAsArrayBuffer(file); // 拿到的file
  reader.onloadend = async () => {
    const arrayBuffer = reader.result;
    let count = 0;
    try {
      const pdf = await PDFDocument.load(arrayBuffer, {
        ignoreEncryption: true,
      });
      count = pdf.getPageCount();
    } catch (e) {
      count =
        arrayBufferToString(arrayBuffer)?.match(/\/Type[\s]*\/Page[^s]/g)
          ?.length || 0;
    }
    if (count > PDF_LIMITS.pages) {
      message.error(
        formatMessage(
          {
            defaultMessage: "文件页数超过 {maxSize} 页,请重新上传",
            id: "hyIgXE",
          },
          { maxSize: PDF_LIMITS.pages }
        )
      );
      reject();
    }
    const blob = file.slice(0, file.size, file.type);
    const encodedFileName = encodeURIComponent(file.name);
    const newFile = new File([blob], encodedFileName, { type: file.type });
    resolve(newFile);
  };
  reader.onerror = (error) => reject(error);
});

PDFJS+React+antd改造

/* eslint-disable no-underscore-dangle */
import {
  CloseOutlined,
  DownloadOutlined,
  LeftOutlined,
  MinusOutlined,
  PlusOutlined,
  RightOutlined,
  SearchOutlined,
} from '@ant-design/icons';
import { ProCard } from '@ant-design/pro-components';
import { API, api, StatusCode } from '@yanyin/service';
import { Content, Layout } from '@yanyin/ui-basic';
import { useDebounceFn, useRequest } from 'ahooks';
import type { InputNumberProps, LayoutProps, SelectProps } from 'antd';
import {
  Button,
  Checkbox,
  Flex,
  Input,
  InputNumber,
  Popover,
  Select,
  Space,
  Spin,
  Typography,
} from 'antd';
import type { PDFDocumentProxy } from 'pdfjs-dist';
import * as pdfjs from 'pdfjs-dist';
import { getPdfFilenameFromUrl } from 'pdfjs-dist';
import {
  DownloadManager,
  EventBus,
  PDFFindController,
  PDFLinkService,
  PDFViewer,
} from 'pdfjs-dist/web/pdf_viewer.mjs';
import React, { useEffect, useRef, useState, type FC } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import { useNavigate } from 'react-router-dom';
import './pdf_viewer.css';

interface PdfPreviewProps {
  url?: string;
  fileId?: number;
  attachmentId?: number;
  fileName?: string;
  searchAble?: boolean;
  scaleAble?: boolean;
  downloadAble?: boolean;
  extra?: React.ReactNode;
  toolbarProps?: LayoutProps;
}

const DEFAULT_SCALE_VALUE = 'auto';
const PAGE_FIT = 'page-fit';
const MIN_SCALE = 0.1;
const MAX_SCALE = 10.0;

enum FindState {
  FOUND = 0,
  NOT_FOUND = 1,
  WRAPPED = 2,
  PENDING = 3,
}

const PdfPreview: FC<PdfPreviewProps> = ({
  url,
  fileId,
  attachmentId,
  fileName,
  searchAble = true,
  scaleAble = true,
  downloadAble = false,
  extra,
  toolbarProps = {},
}) => {
  const navigator = useNavigate();
  const { formatMessage } = useIntl();
  const [pageNum, setPageNum] = useState(0);
  const [open, setOpen] = useState(false);
  const [scale, setScale] = useState<number | string>(DEFAULT_SCALE_VALUE);
  const [currentPage, setCurrentPage] = useState<number>(0);
  const [pdfLoading, setPdfLoading] = useState(true);
  const [searchLoading, setSearchLoading] = useState(false);
  const [matches, setMatches] = useState<{ current: number; total: number }>({
    total: 0,
    current: 0,
  });
  const [searcher, setSearcher] = useState({
    query: '',
    highlightAll: true, // 全部高亮显示
    caseSensitive: false, // 大小写敏感
    entireWord: false, // 全词匹配
    matchDiacritics: false, // 匹配变音符号
  });
  const mainContainerRef = useRef<HTMLDivElement>(null);
  const viewerContainerRef = useRef<HTMLDivElement>(null);
  const inputRef = useRef<HTMLInputElement>(null);
  const windowAbortControllerRef = useRef(new AbortController());
  const eventBusRef = useRef<EventBus>();
  const downloadManagerRef = useRef<DownloadManager>();
  const pdfLinkServiceRef = useRef<PDFLinkService>();
  const pdfDocumentRef = useRef<PDFDocumentProxy>();
  const pdfViewerRef = useRef<PDFViewer>();

  const options = [
    {
      label: formatMessage({ defaultMessage: '自动缩放', id: 'Xvrkin' }),
      value: DEFAULT_SCALE_VALUE,
    },
    {
      label: formatMessage({ defaultMessage: '实际大小', id: 'KgIFHV' }),
      value: 'page-actual',
    },
    {
      label: formatMessage({ defaultMessage: '适合页面', id: '45oOyT' }),
      value: PAGE_FIT,
    },
    {
      label: formatMessage({ defaultMessage: '适合页宽', id: 'fi+lRl' }),
      value: 'page-width',
    },
    {
      label: formatMessage({ defaultMessage: '50%', id: '+iy0zp' }),
      value: 0.5,
    },
    {
      label: formatMessage({ defaultMessage: '75%', id: 'nkl3al' }),
      value: 0.75,
    },
    {
      label: formatMessage({ defaultMessage: '100%', id: '8ZVfG8' }),
      value: 1,
    },
    {
      label: formatMessage({ defaultMessage: '125%', id: 'wcyEbJ' }),
      value: 1.25,
    },
    {
      label: formatMessage({ defaultMessage: '150%', id: 'j02hp4' }),
      value: 1.5,
    },
    {
      label: formatMessage({ defaultMessage: '200%', id: 'IfpyNV' }),
      value: 2,
    },
    {
      label: formatMessage({ defaultMessage: '300%', id: 'RzcXh4' }),
      value: 3,
    },
    {
      label: formatMessage({ defaultMessage: '400%', id: 'ezsVYF' }),
      value: 4,
    },
  ];

  const onNext = () => {
    pdfViewerRef.current?.nextPage();
  };

  const onPrev = () => {
    pdfViewerRef.current?.previousPage();
  };

  const goToPage: InputNumberProps['onChange'] = (value) => {
    if (value) {
      pdfLinkServiceRef.current?.goToPage(value);
    }
  };

  const zoomIn = () => {
    pdfViewerRef.current?.increaseScale();
  };

  const zoomOut = () => {
    pdfViewerRef.current?.decreaseScale();
  };

  const onScaleChange: SelectProps['onChange'] = (value) => {
    pdfViewerRef.current!.currentScaleValue = value;
  };

  const onSearcherChange = (
    param: Record<string, any> = {},
    type = 'again',
    findPrevious = false,
  ) => {
    const newSearcher = { ...searcher, ...param };
    setSearcher(newSearcher);
    eventBusRef?.current?.dispatch('find', {
      type,
      findPrevious,
      ...newSearcher,
    });
  };

  const { run: onChange } = useDebounceFn(
    (...args) => {
      onSearcherChange(...args);
    },
    { wait: 100 },
  );

  const onKeyDown = (evt) => {
    const cmd =
      (evt.ctrlKey ? 1 : 0) ||
      (evt.altKey ? 2 : 0) ||
      (evt.shiftKey ? 4 : 0) ||
      (evt.metaKey ? 8 : 0);
    // 缩放设置为适合页面时,会监听上下按键事件
    if (cmd === 0) {
      let turnPage = 0;
      let turnOnlyIfPageFit = false;
      switch (evt.keyCode) {
        case 40:
        case 34:
          turnOnlyIfPageFit = true;
          turnPage = 1;
          break;
        case 38:
        case 33:
          turnOnlyIfPageFit = true;
          turnPage = -1;
          break;
        default:
          break;
      }
      if (
        turnPage !== 0 &&
        (!turnOnlyIfPageFit || pdfViewerRef.current!.currentScaleValue === PAGE_FIT)
      ) {
        if (turnPage > 0) {
          pdfViewerRef.current?.nextPage();
        } else {
          pdfViewerRef.current?.previousPage();
        }
        evt.preventDefault();
      }
    }
  };

  const bindWindowEvents = () => {
    window.addEventListener('resize', () =>
      eventBusRef?.current?.dispatch('resize', { source: window }),
    );
    window.addEventListener('keydown', onKeyDown);
  };

  const onLoad = (url: string) => {
    const linkService = new PDFLinkService();
    const eventBus = new EventBus();
    const downloadManager = new DownloadManager();
    const findController = new PDFFindController({
      eventBus,
      linkService,
    });
    const pdfViewer = new PDFViewer({
      container: mainContainerRef.current,
      viewer: viewerContainerRef.current,
      linkService,
      eventBus,
      findController,
      downloadManager,
    });
    linkService.setViewer(pdfViewer);
    const loadingTask = pdfjs.getDocument({
      url,
    });
    loadingTask.onProgress = ({ loaded, total }: { loaded: number; total: number }) => {
      setPdfLoading(loaded !== total);
    };
    loadingTask.promise.then(async (pdfDocument) => {
      if (pdfDocument) {
        const pageLayoutPromise = pdfDocument.getPageLayout().catch(() => {});
        const pageModePromise = pdfDocument.getPageMode().catch(() => {});
        const openActionPromise = pdfDocument.getOpenAction().catch(() => {});
        const animationStarted = new Promise((resolve) => {
          window.requestAnimationFrame(resolve);
        });
        const num = pdfDocument.numPages;
        setPageNum(num);
        setCurrentPage(1);
        pdfViewer.setDocument(pdfDocument);
        linkService.setDocument(pdfDocument);
        downloadManagerRef.current = downloadManager;
        pdfDocumentRef.current = pdfDocument;
        pdfLinkServiceRef.current = linkService;
        const { firstPagePromise, pagesPromise } = pdfViewer;
        firstPagePromise.then(() => {
          Promise.all([
            animationStarted,
            pageLayoutPromise,
            pageModePromise,
            openActionPromise,
          ]).then(async () => {
            await Promise.race([
              pagesPromise,
              new Promise((resolve) => {
                setTimeout(resolve, 1000);
              }),
            ]);
            pdfViewer.currentScaleValue = DEFAULT_SCALE_VALUE;
            pdfViewerRef.current = pdfViewer;
            // 缩放监听函数
            eventBus?._on('resize', () => {
              if (!pdfDocument) return;
              const { currentScaleValue = 1 } = pdfViewer;
              if (
                currentScaleValue === DEFAULT_SCALE_VALUE ||
                currentScaleValue === PAGE_FIT ||
                currentScaleValue === 'page-width'
              ) {
                pdfViewer!.currentScaleValue = currentScaleValue;
              }
              pdfViewer?.update();
            });
            // 翻页监听函数
            eventBus?._on('pagechanging', (evt: { pageNumber: number }) => {
              setCurrentPage(evt.pageNumber);
            });
            // 首次匹配监听函数
            eventBus?._on(
              'updatefindmatchescount',
              (evt: { matchesCount: { total: number; current: number } }) => {
                setMatches(evt.matchesCount);
              },
            );
            // 全文匹配监听函数
            eventBus?._on(
              'updatefindcontrolstate',
              (evt: { matchesCount: { total: number; current: number }; state: number }) => {
                if (evt.state === FindState.PENDING) {
                  setSearchLoading(true);
                } else {
                  setSearchLoading(false);
                  setMatches(evt.matchesCount);
                }
              },
            );
            // 缩放监听函数
            eventBus?._on(
              'scalechanging',
              (evt: { scale: number | string; presetValue?: string }) => {
                setScale(
                  evt?.presetValue ||
                    (options.map((i) => i.value).includes(evt.scale)
                      ? evt.scale
                      : `${Math.floor((evt.scale || 1) * 100)}%`),
                );
              },
            );
            eventBusRef.current = eventBus;
            bindWindowEvents();
          });
        });
      }
    });
  };

  const { data: pdfUrl = '' } = useRequest(
    async () => {
      if (url) return url;
      let curAttachmentId = attachmentId;
      if (fileId) {
        const fileInfo = await api.file.getFileInfo(API.File.FileTypeEnum.PDF, fileId);
        curAttachmentId = fileInfo.fileContent?.attachmentId;
      }
      const binaryData = await api.file.downloadAttachment(curAttachmentId!);
      const blob = new Blob([binaryData]);
      return new Promise((resolve, reject) => {
        const fileReader = new FileReader();
        fileReader.onload = (e) => {
          resolve(e.target.result);
        };
        // readAsDataURL
        fileReader.readAsDataURL(blob);
        fileReader.onerror = () => {
          reject(new Error('blobToBase64 error'));
        };
      });
    },
    {
      ready: !!fileId || !!attachmentId || !!url,
      onSuccess: (url: string) => {
        onLoad(url);
      },
      onError: (e) => {
        if (e?.code === StatusCode.UNAUTHORIZED) {
          navigator('/login');
        }
      },
    },
  );

  const onDownload = async () => {
    const data = await pdfDocumentRef.current?.getData();
    const { contentDispositionFilename } = (await pdfDocumentRef.current?.getMetadata()) as any;
    downloadManagerRef.current?.download(
      data,
      pdfUrl,
      fileName || contentDispositionFilename || getPdfFilenameFromUrl(pdfUrl),
    );
  };

  useEffect(() => {
    pdfjs.GlobalWorkerOptions.workerSrc = new URL(
      'pdfjs-dist/build/pdf.worker.min.mjs',
      import.meta.url,
    ).toString();
    return () => {
      windowAbortControllerRef.current.abort();
    };
  }, []);

  useEffect(() => {
    if (open) {
      inputRef?.current?.focus();
    }
  }, [open]);

  const { className, ...restToolbarProps } = toolbarProps;

  return (
    <Spin spinning={pdfLoading} size="large">
      <Layout className="h-screen">
        <Layout.Header
          className={`h-12 bg-white px-4 leading-[48px] ${className}`}
          {...restToolbarProps}
        >
          <Flex justify="space-between" align="center" className="h-full">
            {extra}
            <Space size={8}>
              {searchAble && (
                <Popover
                  arrow={false}
                  open={open}
                  trigger="click"
                  placement="bottomRight"
                  content={
                    <ProCard
                      className="w-[320px]"
                      bodyStyle={{ paddingInline: 0, paddingBlockEnd: 0 }}
                      headStyle={{ padding: 0 }}
                      title={formatMessage({ defaultMessage: '检索', id: 'Uzdp3n' })}
                      extra={
                        <CloseOutlined
                          onClick={() => {
                            setOpen(false);
                            onChange({ query: '' });
                          }}
                        />
                      }
                    >
                      <Flex gap={16} vertical>
                        <Input
                          prefix={<SearchOutlined />}
                          placeholder={formatMessage({ defaultMessage: '检索全文', id: 'liP7lJ' })}
                          onChange={(e) => onChange({ query: e.target.value })}
                          ref={inputRef}
                        />
                        <Flex vertical>
                          <Checkbox
                            checked={searcher.highlightAll}
                            onChange={(e) =>
                              onChange({ highlightAll: e.target.checked }, 'highlightallchange')
                            }
                          >
                            <FormattedMessage defaultMessage="全部高亮显示" id="OKjoI+" />
                          </Checkbox>
                          <Checkbox
                            checked={searcher.caseSensitive}
                            onChange={(e) =>
                              onChange({ caseSensitive: e.target.checked }, 'casesensitivitychange')
                            }
                          >
                            <FormattedMessage defaultMessage="区分大小写" id="9ovhff" />
                          </Checkbox>
                          <Checkbox
                            checked={searcher.matchDiacritics}
                            onChange={(e) =>
                              onChange(
                                { matchDiacritics: e.target.checked },
                                'diacriticmatchingchange',
                              )
                            }
                          >
                            <FormattedMessage defaultMessage="匹配变音符号" id="DUnzL9" />
                          </Checkbox>
                          <Checkbox
                            checked={searcher.entireWord}
                            onChange={(e) =>
                              onChange({ entireWord: e.target.checked }, 'entirewordchange')
                            }
                          >
                            <FormattedMessage defaultMessage="全词匹配" id="08ZjOK" />
                          </Checkbox>
                        </Flex>
                        <Flex justify="space-between" align="center">
                          <Typography.Text type="success">{`${matches.current} / ${matches?.total}`}</Typography.Text>
                          <Space size={16}>
                            <Button
                              onClick={() => onChange({}, 'again', true)}
                              disabled={searchLoading}
                            >
                              <FormattedMessage defaultMessage="上一个" id="U2X/B+" />
                            </Button>
                            <Button onClick={() => onChange()} disabled={searchLoading}>
                              <FormattedMessage defaultMessage="下一个" id="XjKOCc" />
                            </Button>
                          </Space>
                        </Flex>
                      </Flex>
                    </ProCard>
                  }
                >
                  <Button
                    icon={<SearchOutlined />}
                    onClick={() => setOpen(!open)}
                    className={
                      open ? '[&_.ant-btn-icon]:text-primary border-transparent bg-[#e6f7eb]' : ''
                    }
                  />
                </Popover>
              )}
              {scaleAble && (
                <Space.Compact>
                  <Button
                    className="p-0"
                    icon={<MinusOutlined />}
                    onClick={zoomOut}
                    disabled={+scale <= MIN_SCALE}
                  />
                  <Select
                    onChange={onScaleChange}
                    value={scale}
                    options={options}
                    className="min-w-24"
                  />
                  <Button
                    className="p-0"
                    icon={<PlusOutlined />}
                    onClick={zoomIn}
                    disabled={+scale >= MAX_SCALE}
                  />
                </Space.Compact>
              )}
            </Space>
            <Space>
              <Button
                className="p-0"
                icon={<LeftOutlined />}
                onClick={onPrev}
                size="small"
                type="link"
                disabled={currentPage <= 1}
              />
              <InputNumber
                className={pageNum < 10 ? 'w-8' : pageNum < 100 ? 'w-10' : 'w-12'}
                value={currentPage}
                onChange={goToPage}
                size="small"
                controls={false}
              />
              <span className="mx-1">/</span>
              <span>{pageNum}</span>
              <Button
                className="p-0"
                icon={<RightOutlined />}
                onClick={onNext}
                size="small"
                type="link"
                disabled={currentPage >= pageNum}
              />
            </Space>
            {downloadAble && (
              <Button icon={<DownloadOutlined />} onClick={onDownload}>
                <FormattedMessage defaultMessage="文件下载" id="6fYWQx" />
              </Button>
            )}
          </Flex>
        </Layout.Header>
        <Content autoScroll showBackground showPadding={false}>
          <div id="viewerContainer" ref={mainContainerRef}>
            <div id="viewer" className="pdfViewer" ref={viewerContainerRef} />
          </div>
        </Content>
      </Layout>
    </Spin>
  );
};

export default PdfPreview;