mirror of https://github.com/fantasticit/think.git
feat: support preview pdf, image, audio and video
parent
b290a2305d
commit
39cf943eaa
|
@ -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",
|
||||
|
|
|
@ -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<IProps> = ({ containerSelector }) => {
|
||||
export const ImageViewer: React.FC<IProps> = ({ 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<IProps> = ({ containerSelector }) => {
|
|||
io.disconnect();
|
||||
viewer.destroy();
|
||||
};
|
||||
}, [containerSelector]);
|
||||
}, [container, containerSelector]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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 <IconSong />;
|
||||
|
||||
case 'video':
|
||||
return <IconVideo />;
|
||||
|
||||
case 'file':
|
||||
return <IconFile />;
|
||||
|
||||
case 'image':
|
||||
return <IconImage />;
|
||||
|
||||
case 'pdf':
|
||||
return <IconFile />;
|
||||
|
||||
default: {
|
||||
const value: never = type;
|
||||
throw new Error(value);
|
||||
}
|
||||
}
|
||||
};
|
|
@ -1,12 +1,17 @@
|
|||
.wrap {
|
||||
border: 1px solid var(--semi-color-border);
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
&.isPreviewing {
|
||||
&::after {
|
||||
background-color: transparent !important ;
|
||||
}
|
||||
}
|
||||
|
||||
> div:first-child {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 <IconSong />;
|
||||
|
||||
case 'video':
|
||||
return <IconVideo />;
|
||||
|
||||
case 'file':
|
||||
return <IconFile />;
|
||||
|
||||
case 'image':
|
||||
return <IconImage />;
|
||||
|
||||
default: {
|
||||
const value: never = type;
|
||||
throw new Error(value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const AttachmentWrapper = ({ editor, node, updateAttributes }) => {
|
||||
const $upload = useRef<HTMLInputElement>();
|
||||
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 (
|
||||
<div className={cls(styles.wrap, 'render-wrapper')} onClick={selectFile}>
|
||||
<Text>{error}</Text>
|
||||
|
@ -99,17 +64,17 @@ export const AttachmentWrapper = ({ editor, node, updateAttributes }) => {
|
|||
if (url) {
|
||||
return (
|
||||
<>
|
||||
<div className={cls(styles.wrap, 'render-wrapper')} onClick={selectFile}>
|
||||
<div className={cls(styles.wrap, visible && styles.isPreviewing, 'render-wrapper')} onClick={selectFile}>
|
||||
<div>
|
||||
<Space>
|
||||
{getFileTypeIcon(type)}
|
||||
{getFileTypeIcon(fileType)}
|
||||
<Text ellipsis={{ showTooltip: true }} style={{ maxWidth: 320 }}>
|
||||
{fileName}.{fileExt}
|
||||
</Text>
|
||||
<Text type="tertiary"> ({normalizeFileSize(fileSize)})</Text>
|
||||
</Space>
|
||||
<span>
|
||||
{type === 'video' || type === 'audio' ? (
|
||||
<Tooltip content={!visible ? '播放' : '收起'}>
|
||||
<Tooltip content={!visible ? '预览' : '收起'}>
|
||||
<Button
|
||||
theme={'borderless'}
|
||||
type="tertiary"
|
||||
|
@ -117,7 +82,6 @@ export const AttachmentWrapper = ({ editor, node, updateAttributes }) => {
|
|||
onClick={toggleVisible}
|
||||
/>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
<Tooltip content="下载">
|
||||
<Button
|
||||
theme={'borderless'}
|
||||
|
@ -128,13 +92,12 @@ export const AttachmentWrapper = ({ editor, node, updateAttributes }) => {
|
|||
</Tooltip>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{url ? (
|
||||
<Collapsible isOpen={visible}>
|
||||
{type === 'video' && <video controls autoPlay src={url}></video>}
|
||||
{type === 'audio' && <audio controls autoPlay src={url}></audio>}
|
||||
<Player fileType={fileType} url={url} />
|
||||
</Collapsible>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
}
|
|
@ -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<IProps> = ({ url, fileType }) => {
|
||||
const ref = useRef();
|
||||
const type = useMemo(() => normalizeFileType(fileType), [fileType]);
|
||||
|
||||
const player = useMemo(() => {
|
||||
if (type === 'video') return <video controls autoPlay src={url}></video>;
|
||||
|
||||
if (type === 'audio') return <audio controls autoPlay src={url}></audio>;
|
||||
|
||||
if (type === 'image')
|
||||
return <img style={{ width: 'auto', height: 'auto', maxWidth: '100%', maxHeight: 300 }} src={url} />;
|
||||
|
||||
if (type === 'pdf') return <PDFPlayer url={url} />;
|
||||
|
||||
return <Text type="tertiary">暂不支持预览该类型文件</Text>;
|
||||
}, [type, url]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={ref} className={styles.playerWrap}>
|
||||
{player}
|
||||
</div>
|
||||
<ImageViewer container={ref.current} />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<IProps> = ({ url }) => {
|
||||
const [total, setTotal] = useState(1);
|
||||
const [pageNumber, setPageNumber] = useState(1);
|
||||
|
||||
function onDocumentLoadSuccess({ numPages }) {
|
||||
setTotal(numPages);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.playerWrap}>
|
||||
<Document file={url} onLoadSuccess={onDocumentLoadSuccess}>
|
||||
<Page pageNumber={pageNumber} />
|
||||
</Document>
|
||||
<div className={styles.paginationWrap}>
|
||||
<Pagination total={total} pageSize={1} onChange={(page) => setPageNumber(page)} size="small"></Pagination>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue