From 39cf943eaa04cf0bc8dbab9872fbd5772ad6b6c2 Mon Sep 17 00:00:00 2001 From: fantasticit Date: Fri, 1 Apr 2022 16:48:07 +0800 Subject: [PATCH] feat: support preview pdf, image, audio and video --- packages/client/package.json | 1 + .../client/src/components/image-viewer.tsx | 9 +- packages/client/src/tiptap/services/file.ts | 4 +- .../tiptap/wrappers/attachment/file-icon.tsx | 28 ++++++ .../wrappers/attachment/index.module.scss | 17 ++-- .../src/tiptap/wrappers/attachment/index.tsx | 99 ++++++------------- .../attachment/player/index.module.scss | 12 +++ .../wrappers/attachment/player/index.tsx | 46 +++++++++ .../player/pdf-player/index.module.scss | 37 +++++++ .../attachment/player/pdf-player/index.tsx | 30 ++++++ pnpm-lock.yaml | 68 +++++++++++++ 11 files changed, 272 insertions(+), 79 deletions(-) create mode 100644 packages/client/src/tiptap/wrappers/attachment/file-icon.tsx create mode 100644 packages/client/src/tiptap/wrappers/attachment/player/index.module.scss create mode 100644 packages/client/src/tiptap/wrappers/attachment/player/index.tsx create mode 100644 packages/client/src/tiptap/wrappers/attachment/player/pdf-player/index.module.scss create mode 100644 packages/client/src/tiptap/wrappers/attachment/player/pdf-player/index.tsx diff --git a/packages/client/package.json b/packages/client/package.json index 827712bd..9934d7cf 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -76,6 +76,7 @@ "react": "17.0.2", "react-dom": "17.0.2", "react-helmet": "^6.1.0", + "react-pdf": "^5.7.2", "react-split-pane": "^0.1.92", "scroll-into-view-if-needed": "^2.2.29", "swr": "^1.2.0", diff --git a/packages/client/src/components/image-viewer.tsx b/packages/client/src/components/image-viewer.tsx index fe184239..4e25c693 100644 --- a/packages/client/src/components/image-viewer.tsx +++ b/packages/client/src/components/image-viewer.tsx @@ -2,12 +2,13 @@ import { useEffect } from 'react'; import Viewer from 'viewerjs'; interface IProps { - containerSelector: string; + containerSelector?: string; + container?: HTMLElement; } -export const ImageViewer: React.FC = ({ containerSelector }) => { +export const ImageViewer: React.FC = ({ container, containerSelector }) => { useEffect(() => { - const el = document.querySelector(containerSelector); + const el = container || document.querySelector(containerSelector); if (!el) { return null; } @@ -24,7 +25,7 @@ export const ImageViewer: React.FC = ({ containerSelector }) => { io.disconnect(); viewer.destroy(); }; - }, [containerSelector]); + }, [container, containerSelector]); return null; }; diff --git a/packages/client/src/tiptap/services/file.ts b/packages/client/src/tiptap/services/file.ts index b8b86e86..fab8f684 100644 --- a/packages/client/src/tiptap/services/file.ts +++ b/packages/client/src/tiptap/services/file.ts @@ -31,11 +31,13 @@ export const normalizeFileSize = (size) => { return (size / 1024 / 1024).toFixed(2) + ' MB'; }; -export type FileType = 'image' | 'audio' | 'video' | 'file'; +export type FileType = 'image' | 'audio' | 'video' | 'pdf' | 'file'; export const normalizeFileType = (fileType): FileType => { if (!fileType) return 'file'; + if (fileType === 'application/pdf') return 'pdf'; + if (fileType.startsWith('image')) { return 'image'; } diff --git a/packages/client/src/tiptap/wrappers/attachment/file-icon.tsx b/packages/client/src/tiptap/wrappers/attachment/file-icon.tsx new file mode 100644 index 00000000..a19a3e65 --- /dev/null +++ b/packages/client/src/tiptap/wrappers/attachment/file-icon.tsx @@ -0,0 +1,28 @@ +import { IconFile, IconSong, IconVideo, IconImage } from '@douyinfe/semi-icons'; +import { normalizeFileType } from '../../services/file'; + +export const getFileTypeIcon = (fileType: string) => { + const type = normalizeFileType(fileType); + + switch (type) { + case 'audio': + return ; + + case 'video': + return ; + + case 'file': + return ; + + case 'image': + return ; + + case 'pdf': + return ; + + default: { + const value: never = type; + throw new Error(value); + } + } +}; diff --git a/packages/client/src/tiptap/wrappers/attachment/index.module.scss b/packages/client/src/tiptap/wrappers/attachment/index.module.scss index b2527f9c..076a440a 100644 --- a/packages/client/src/tiptap/wrappers/attachment/index.module.scss +++ b/packages/client/src/tiptap/wrappers/attachment/index.module.scss @@ -1,12 +1,17 @@ .wrap { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px 16px; border: 1px solid var(--semi-color-border); border-radius: var(--border-radius); - &:hover { - border: 1px solid var(--semi-color-link); + &.isPreviewing { + &::after { + background-color: transparent !important ; + } + } + + > div:first-child { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 16px; } } diff --git a/packages/client/src/tiptap/wrappers/attachment/index.tsx b/packages/client/src/tiptap/wrappers/attachment/index.tsx index 71d53a1b..8d6a226f 100644 --- a/packages/client/src/tiptap/wrappers/attachment/index.tsx +++ b/packages/client/src/tiptap/wrappers/attachment/index.tsx @@ -2,51 +2,18 @@ import { useEffect, useRef } from 'react'; import cls from 'classnames'; import { NodeViewWrapper, NodeViewContent } from '@tiptap/react'; import { Button, Typography, Spin, Collapsible, Space } from '@douyinfe/semi-ui'; -import { - IconDownload, - IconPlayCircle, - IconFile, - IconSong, - IconVideo, - IconImage, - IconClose, -} from '@douyinfe/semi-icons'; +import { IconDownload, IconPlayCircle, IconClose } from '@douyinfe/semi-icons'; import { Tooltip } from 'components/tooltip'; import { useToggle } from 'hooks/use-toggle'; import { download } from '../../services/download'; import { uploadFile } from 'services/file'; -import { - normalizeFileSize, - extractFileExtension, - extractFilename, - normalizeFileType, - FileType, -} from '../../services/file'; +import { normalizeFileSize, extractFileExtension, extractFilename } from '../../services/file'; +import { Player } from './player'; +import { getFileTypeIcon } from './file-icon'; import styles from './index.module.scss'; const { Text } = Typography; -const getFileTypeIcon = (type: FileType) => { - switch (type) { - case 'audio': - return ; - - case 'video': - return ; - - case 'file': - return ; - - case 'image': - return ; - - default: { - const value: never = type; - throw new Error(value); - } - } -}; - export const AttachmentWrapper = ({ editor, node, updateAttributes }) => { const $upload = useRef(); const isEditable = editor.isEditable; @@ -78,8 +45,6 @@ export const AttachmentWrapper = ({ editor, node, updateAttributes }) => { } }; - const type = normalizeFileType(fileType); - useEffect(() => { if (!url && !hasTrigger) { selectFile(); @@ -88,7 +53,7 @@ export const AttachmentWrapper = ({ editor, node, updateAttributes }) => { }, [url, hasTrigger]); const content = (() => { - if (error) { + if (error !== 'null') { return (
{error} @@ -99,17 +64,17 @@ export const AttachmentWrapper = ({ editor, node, updateAttributes }) => { if (url) { return ( <> -
- - {getFileTypeIcon(type)} - - {fileName}.{fileExt} - - ({normalizeFileSize(fileSize)}) - - - {type === 'video' || type === 'audio' ? ( - +
+
+ + {getFileTypeIcon(fileType)} + + {fileName}.{fileExt} + + ({normalizeFileSize(fileSize)}) + + +
+ {url ? ( + + + + ) : null}
- - {url ? ( - - {type === 'video' && } - {type === 'audio' && } - - ) : null} ); } diff --git a/packages/client/src/tiptap/wrappers/attachment/player/index.module.scss b/packages/client/src/tiptap/wrappers/attachment/player/index.module.scss new file mode 100644 index 00000000..e432b8ef --- /dev/null +++ b/packages/client/src/tiptap/wrappers/attachment/player/index.module.scss @@ -0,0 +1,12 @@ +.playerWrap { + width: 100%; + display: flex; + justify-content: center; + border-top: 1px solid var(--semi-color-border); + padding: 12px; + + > video { + width: 100%; + height: 100%; + } +} diff --git a/packages/client/src/tiptap/wrappers/attachment/player/index.tsx b/packages/client/src/tiptap/wrappers/attachment/player/index.tsx new file mode 100644 index 00000000..4edc5a88 --- /dev/null +++ b/packages/client/src/tiptap/wrappers/attachment/player/index.tsx @@ -0,0 +1,46 @@ +import React, { useMemo, useRef } from 'react'; +import { Typography } from '@douyinfe/semi-ui'; +import { ImageViewer } from 'components/image-viewer'; +import { + normalizeFileSize, + extractFileExtension, + extractFilename, + normalizeFileType, + FileType, +} from '../../../services/file'; +import { PDFPlayer } from './pdf-player'; +import styles from './index.module.scss'; + +interface IProps { + url: string; + fileType: string; +} + +const { Text } = Typography; + +export const Player: React.FC = ({ url, fileType }) => { + const ref = useRef(); + const type = useMemo(() => normalizeFileType(fileType), [fileType]); + + const player = useMemo(() => { + if (type === 'video') return ; + + if (type === 'audio') return ; + + if (type === 'image') + return ; + + if (type === 'pdf') return ; + + return 暂不支持预览该类型文件; + }, [type, url]); + + return ( + <> +
+ {player} +
+ + + ); +}; diff --git a/packages/client/src/tiptap/wrappers/attachment/player/pdf-player/index.module.scss b/packages/client/src/tiptap/wrappers/attachment/player/pdf-player/index.module.scss new file mode 100644 index 00000000..03c96e30 --- /dev/null +++ b/packages/client/src/tiptap/wrappers/attachment/player/pdf-player/index.module.scss @@ -0,0 +1,37 @@ +.playerWrap { + width: 100%; + + :global { + .react-pdf__Document { + display: flex; + flex-direction: column; + align-items: center; + border-radius: 8px; + } + + .react-pdf__Page { + display: flex; + justify-content: center; + width: 100%; + height: 420px; + overflow: auto; + } + + .react-pdf__Page canvas { + max-width: 100%; + height: auto !important; + } + + .react-pdf__message { + padding: 20px; + color: white; + } + } + + .paginationWrap { + margin-top: 1em; + text-align: center; + display: flex; + justify-content: center; + } +} diff --git a/packages/client/src/tiptap/wrappers/attachment/player/pdf-player/index.tsx b/packages/client/src/tiptap/wrappers/attachment/player/pdf-player/index.tsx new file mode 100644 index 00000000..d76ecd98 --- /dev/null +++ b/packages/client/src/tiptap/wrappers/attachment/player/pdf-player/index.tsx @@ -0,0 +1,30 @@ +import React, { useState } from 'react'; +import { Document, Page, pdfjs } from 'react-pdf'; +import { Pagination } from '@douyinfe/semi-ui'; +import styles from './index.module.scss'; + +pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/legacy/build/pdf.worker.min.js`; + +interface IProps { + url: string; +} + +export const PDFPlayer: React.FC = ({ url }) => { + const [total, setTotal] = useState(1); + const [pageNumber, setPageNumber] = useState(1); + + function onDocumentLoadSuccess({ numPages }) { + setTotal(numPages); + } + + return ( +
+ + + +
+ setPageNumber(page)} size="small"> +
+
+ ); +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b14fff8..62028145 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,6 +104,7 @@ importers: react: 17.0.2 react-dom: 17.0.2 react-helmet: ^6.1.0 + react-pdf: ^5.7.2 react-split-pane: ^0.1.92 scroll-into-view-if-needed: ^2.2.29 swr: ^1.2.0 @@ -182,6 +183,7 @@ importers: react: 17.0.2 react-dom: 17.0.2_react@17.0.2 react-helmet: 6.1.0_react@17.0.2 + react-pdf: 5.7.2_react-dom@17.0.2+react@17.0.2 react-split-pane: 0.1.92_react-dom@17.0.2+react@17.0.2 scroll-into-view-if-needed: 2.2.29 swr: 1.2.0_react@17.0.2 @@ -3871,6 +3873,16 @@ packages: flat-cache: 3.0.4 dev: true + /file-loader/6.2.0: + resolution: {integrity: sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==} + engines: {node: '>= 10.13.0'} + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + dependencies: + loader-utils: 2.0.0 + schema-utils: 3.1.1 + dev: false + /file-uri-to-path/2.0.0: resolution: {integrity: sha512-hjPFI8oE/2iQPVe4gbrJ73Pp+Xfub2+WI2LlXDbsaJBwT5wuMh35WNWVYYTpnz895shtwfyutMFLFywpQAFdLg==} engines: {node: '>= 6'} @@ -5591,6 +5603,10 @@ packages: sourcemap-codec: 1.4.8 dev: true + /make-cancellable-promise/1.1.0: + resolution: {integrity: sha512-X5Opjm2xcZsOLuJ+Bnhb4t5yfu4ehlA3OKEYLtqUchgVzL/QaqW373ZUVxVHKwvJ38cmYuR4rAHD2yUvAIkTPA==} + dev: false + /make-dir/3.1.0: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} engines: {node: '>=8'} @@ -5602,6 +5618,10 @@ packages: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} dev: true + /make-event-props/1.3.0: + resolution: {integrity: sha512-oWiDZMcVB1/A487251hEWza1xzgCzl6MXxe9aF24l5Bt9N9UEbqTqKumEfuuLhmlhRZYnc+suVvW4vUs8bwO7Q==} + dev: false + /makeerror/1.0.12: resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} dependencies: @@ -5696,10 +5716,18 @@ packages: yargs-parser: 20.2.9 dev: true + /merge-class-names/1.4.2: + resolution: {integrity: sha512-bOl98VzwCGi25Gcn3xKxnR5p/WrhWFQB59MS/aGENcmUc6iSm96yrFDF0XSNurX9qN4LbJm0R9kfvsQ17i8zCw==} + dev: false + /merge-descriptors/1.0.1: resolution: {integrity: sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=} dev: false + /merge-refs/1.0.0: + resolution: {integrity: sha512-WZ4S5wqD9FCR9hxkLgvcHJCBxzXzy3VVE6p8W2OzxRzB+hLRlcadGE2bW9xp2KSzk10rvp4y+pwwKO6JQVguMg==} + dev: false + /merge-stream/2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} dev: true @@ -6250,6 +6278,15 @@ packages: resolution: {integrity: sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=} dev: false + /pdfjs-dist/2.12.313: + resolution: {integrity: sha512-1x6iXO4Qnv6Eb+YFdN5JdUzt4pAkxSp3aLAYPX93eQCyg/m7QFzXVWJHJVtoW48CI8HCXju4dSkhQZwoheL5mA==} + peerDependencies: + worker-loader: ^3.0.8 + peerDependenciesMeta: + worker-loader: + optional: true + dev: false + /picocolors/0.2.1: resolution: {integrity: sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==} dev: false @@ -6672,6 +6709,29 @@ packages: resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} dev: false + /react-pdf/5.7.2_react-dom@17.0.2+react@17.0.2: + resolution: {integrity: sha512-hdDwvf007V0i2rPCqQVS1fa70CXut17SN3laJYlRHzuqcu8sLLjEoeXihty6c0Ev5g1mw31b8OT8EwRw1s8C4g==} + peerDependencies: + react: ^16.3.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.3.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.16.7 + file-loader: 6.2.0 + make-cancellable-promise: 1.1.0 + make-event-props: 1.3.0 + merge-class-names: 1.4.2 + merge-refs: 1.0.0 + pdfjs-dist: 2.12.313 + prop-types: 15.8.1 + react: 17.0.2 + react-dom: 17.0.2_react@17.0.2 + tiny-invariant: 1.2.0 + tiny-warning: 1.0.3 + transitivePeerDependencies: + - webpack + - worker-loader + dev: false + /react-resizable/1.11.1_react-dom@17.0.2+react@17.0.2: resolution: {integrity: sha512-S70gbLaAYqjuAd49utRHibtHLrHXInh7GuOR+6OO6RO6uleQfuBnWmZjRABfqNEx3C3Z6VPLg0/0uOYFrkfu9Q==} peerDependencies: @@ -7737,6 +7797,14 @@ packages: /through/2.3.8: resolution: {integrity: sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=} + /tiny-invariant/1.2.0: + resolution: {integrity: sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==} + dev: false + + /tiny-warning/1.0.3: + resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + dev: false + /tippy.js/6.3.7: resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} dependencies: