mirror of
https://gitee.com/baidu/amis.git
synced 2024-11-29 18:48:45 +08:00
feat: 支持PDF预览
This commit is contained in:
parent
e2db651475
commit
f9be2996af
54
docs/zh-CN/components/pdf-viewer.md
Normal file
54
docs/zh-CN/components/pdf-viewer.md
Normal file
@ -0,0 +1,54 @@
|
||||
---
|
||||
title: PDF Viewer
|
||||
description:
|
||||
type: 0
|
||||
group: ⚙ 组件
|
||||
menuName: PDFViewer 渲染
|
||||
icon:
|
||||
order: 24
|
||||
---
|
||||
|
||||
## 基本用法
|
||||
|
||||
```schema: scope="body"
|
||||
{
|
||||
"type": "pdf-viewer",
|
||||
"id": "pdf-viewer",
|
||||
"src": "/examples/static/simple.pdf"
|
||||
}
|
||||
```
|
||||
|
||||
## 配合文件上传实现预览功能
|
||||
|
||||
配置和 `input-file` 相同的 `name` 即可
|
||||
|
||||
```schema: scope="body"
|
||||
{
|
||||
"type": "form",
|
||||
"title": "",
|
||||
"wrapWithPanel": false,
|
||||
"body": [
|
||||
{
|
||||
"type": "input-file",
|
||||
"name": "file",
|
||||
"label": "File",
|
||||
"asBlob": true,
|
||||
"accept": ".pdf"
|
||||
},
|
||||
{
|
||||
"type": "pdf-viewer",
|
||||
"id": "pdf-viewer",
|
||||
"name": "file"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 属性表
|
||||
|
||||
| 属性名 | 类型 | 默认值 | 说明 |
|
||||
| ---------- | ------ | ------ | ---------- |
|
||||
| src | Api | | 文档地址 |
|
||||
| width | number | 500 | 宽度 |
|
||||
| height | number | - | 高度 |
|
||||
| background | string | #fff | PDF 背景色 |
|
@ -987,6 +987,13 @@ export const components = [
|
||||
import('../../docs/zh-CN/components/office-viewer.md').then(wrapDoc)
|
||||
)
|
||||
},
|
||||
{
|
||||
label: 'PDFViewer 渲染',
|
||||
path: '/zh-CN/components/pdf-viewer',
|
||||
component: React.lazy(() =>
|
||||
import('../../docs/zh-CN/components/pdf-viewer.md').then(wrapDoc)
|
||||
)
|
||||
},
|
||||
{
|
||||
label: 'Progress 进度条',
|
||||
path: '/zh-CN/components/progress',
|
||||
|
@ -130,6 +130,7 @@ import Tab3Schema from './Tabs/Tab3';
|
||||
import Loading from './Loading';
|
||||
import CodeSchema from './Code';
|
||||
import OfficeViewer from './OfficeViewer';
|
||||
import PdfViewer from './PdfViewer';
|
||||
import InputTableEvent from './EventAction/cmpt-event-action/InputTableEvent';
|
||||
import WizardPage from './WizardPage';
|
||||
|
||||
@ -912,6 +913,13 @@ export const examples = [
|
||||
component: makeSchemaRenderer(OfficeViewer)
|
||||
},
|
||||
|
||||
{
|
||||
label: 'Pdf 预览',
|
||||
icon: 'fa fa-file-pdf',
|
||||
path: '/examples/pdf-viewer',
|
||||
component: makeSchemaRenderer(PdfViewer)
|
||||
},
|
||||
|
||||
{
|
||||
label: '多 loading',
|
||||
icon: 'fa fa-spinner',
|
||||
|
23
examples/components/PdfViewer.jsx
Normal file
23
examples/components/PdfViewer.jsx
Normal file
@ -0,0 +1,23 @@
|
||||
export default {
|
||||
type: 'page',
|
||||
body: {
|
||||
type: 'form',
|
||||
id: 'form',
|
||||
debug: true,
|
||||
wrapWithPanel: false,
|
||||
body: [
|
||||
{
|
||||
type: 'input-file',
|
||||
name: 'file.test',
|
||||
label: '选择 PDF 文件预览效果(不会上传到服务器)',
|
||||
asBlob: true,
|
||||
accept: '.pdf'
|
||||
},
|
||||
{
|
||||
type: 'pdf-viewer',
|
||||
id: 'pdf-viewer',
|
||||
name: 'file.test'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
BIN
examples/static/simple.pdf
Normal file
BIN
examples/static/simple.pdf
Normal file
Binary file not shown.
@ -241,7 +241,7 @@ fis.match('/examples/mod.js', {
|
||||
isMod: false
|
||||
});
|
||||
|
||||
fis.match('{markdown-it,moment-timezone}/**', {
|
||||
fis.match('{markdown-it,moment-timezone,pdfjs-dist}/**', {
|
||||
preprocessor: fis.plugin('js-require-file')
|
||||
});
|
||||
|
||||
@ -503,6 +503,7 @@ if (fis.project.currentMedia() === 'publish-sdk') {
|
||||
'!amis-ui/lib/components/RichText.js',
|
||||
'!amis-ui/lib/components/Tinymce.js',
|
||||
'!amis-ui/lib/components/ColorPicker.js',
|
||||
'!amis-ui/lib/components/PdfViewer.js',
|
||||
'!react-color/**',
|
||||
'!material-colors/**',
|
||||
'!reactcss/**',
|
||||
@ -562,6 +563,11 @@ if (fis.project.currentMedia() === 'publish-sdk') {
|
||||
'tinycolor2/**'
|
||||
],
|
||||
|
||||
'pdf-viewer.js': [
|
||||
'amis-ui/lib/components/PdfViewer.js',
|
||||
'pdfjs-dist/build/pdf.worker.min.js'
|
||||
],
|
||||
|
||||
'cropperjs.js': ['cropperjs/**', 'react-cropper/**'],
|
||||
|
||||
'barcode.js': ['src/components/BarCode.tsx', 'jsbarcode/**'],
|
||||
@ -584,6 +590,7 @@ if (fis.project.currentMedia() === 'publish-sdk') {
|
||||
'!mpegts.js/**',
|
||||
'!hls.js/**',
|
||||
'!froala-editor/**',
|
||||
'!pdfjs-dist/**',
|
||||
|
||||
'!amis-ui/lib/components/RichText.js',
|
||||
'!zrender/**',
|
||||
|
@ -65,6 +65,7 @@
|
||||
"react-intersection-observer": "9.5.2",
|
||||
"react-json-view": "1.21.3",
|
||||
"react-overlays": "5.1.1",
|
||||
"react-pdf": "^7.7.1",
|
||||
"react-textarea-autosize": "8.3.3",
|
||||
"react-transition-group": "4.4.2",
|
||||
"react-visibility-sensor": "5.1.1",
|
||||
@ -143,4 +144,4 @@
|
||||
]
|
||||
},
|
||||
"gitHead": "37d23b4a8eb1c663bc38e8dd9040889ea1526ec4"
|
||||
}
|
||||
}
|
||||
|
@ -49,7 +49,8 @@ const input = [
|
||||
'./src/components/Markdown.tsx',
|
||||
'./src/components/Tinymce.tsx',
|
||||
'./src/components/RichText.tsx',
|
||||
'./src/components/CityDB.ts'
|
||||
'./src/components/CityDB.ts',
|
||||
'./src/components/PdfViewer.tsx'
|
||||
];
|
||||
|
||||
/** 获取子包编译后的入口路径,需要使用相对路径 */
|
||||
|
45
packages/amis-ui/scss/components/_pdf_viewer.scss
Normal file
45
packages/amis-ui/scss/components/_pdf_viewer.scss
Normal file
@ -0,0 +1,45 @@
|
||||
.#{$ns}PdfViewer {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
min-height: 500px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 50px 0;
|
||||
&-Content {
|
||||
width: max-content;
|
||||
max-width: 100%;
|
||||
&.is-loaded {
|
||||
box-shadow: rgba(0, 0, 0, 0.2) 0px 2px 4px 0px;
|
||||
}
|
||||
}
|
||||
|
||||
&-Loading {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&-Tool {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
padding: 5px 15px;
|
||||
bottom: 60px;
|
||||
background-color: #444444;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
.gap {
|
||||
margin: 0 4px;
|
||||
}
|
||||
.icon {
|
||||
cursor: pointer;
|
||||
width: 20px;
|
||||
margin: 0 10px;
|
||||
&:hover {
|
||||
color: var(--colors-brand-5);
|
||||
}
|
||||
}
|
||||
.page-input {
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
}
|
@ -142,3 +142,4 @@
|
||||
@import '../components/debug';
|
||||
@import '../components/menu';
|
||||
@import '../components/overflow-tpl';
|
||||
@import '../components/pdf_viewer';
|
||||
|
143
packages/amis-ui/src/components/PdfViewer.tsx
Normal file
143
packages/amis-ui/src/components/PdfViewer.tsx
Normal file
@ -0,0 +1,143 @@
|
||||
/**
|
||||
* @file PdfViewer.tsx PDF 预览
|
||||
*
|
||||
* @created: 2024/02/26
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {themeable, ThemeProps} from 'amis-core';
|
||||
import {Document, Page, pdfjs} from 'react-pdf';
|
||||
|
||||
import {Icon} from './icons';
|
||||
import Input from './Input';
|
||||
import Spinner from './Spinner';
|
||||
// @ts-ignore
|
||||
import pdfJSWorkerURL from 'pdfjs-dist/build/pdf.worker.min';
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = pdfJSWorkerURL;
|
||||
|
||||
export interface PdfViewerProps extends ThemeProps {
|
||||
file?: ArrayBuffer;
|
||||
width?: number;
|
||||
height?: number;
|
||||
background?: string;
|
||||
}
|
||||
|
||||
const PdfViewer: React.FC<PdfViewerProps> = props => {
|
||||
const {classnames: cx, className, width = 500} = props;
|
||||
const [file, setFile] = React.useState(props.file);
|
||||
const [loaded, setLoaded] = React.useState(false);
|
||||
const [page, setPage] = React.useState(1);
|
||||
const [scale, setScale] = React.useState(1);
|
||||
const [total, setTotal] = React.useState(1);
|
||||
const inputRef = React.useRef<HTMLInputElement>();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (props.file instanceof ArrayBuffer && props.file.byteLength > 0) {
|
||||
setFile(props.file);
|
||||
} else {
|
||||
setFile(undefined);
|
||||
}
|
||||
}, [props.file]);
|
||||
|
||||
function handleLoadSuccess({numPages}: {numPages: number}) {
|
||||
setLoaded(true);
|
||||
setTotal(numPages);
|
||||
}
|
||||
|
||||
function handleChangePage(idx: number) {
|
||||
const newPage = page + idx;
|
||||
if (newPage <= 0 || newPage > total) {
|
||||
return;
|
||||
}
|
||||
setPage(newPage);
|
||||
}
|
||||
|
||||
function handlePageBlur(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const newPage = +event.target.value;
|
||||
if (isNaN(newPage) || newPage <= 0 || newPage > total) {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.value = page + '';
|
||||
}
|
||||
return;
|
||||
}
|
||||
setPage(newPage);
|
||||
}
|
||||
|
||||
function handleChangeScale(t: number) {
|
||||
setScale(scale * t);
|
||||
}
|
||||
|
||||
if (!file) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function renderLoading() {
|
||||
return (
|
||||
<div className={cx('PdfViewer-Loading')} style={{width: `${width}px`}}>
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderTool() {
|
||||
return (
|
||||
<div className={cx('PdfViewer-Tool')}>
|
||||
<Icon
|
||||
className="icon"
|
||||
icon="prev"
|
||||
onClick={() => handleChangePage(-1)}
|
||||
/>
|
||||
<Input
|
||||
className="page-input"
|
||||
value={page}
|
||||
onBlur={handlePageBlur}
|
||||
ref={inputRef}
|
||||
/>
|
||||
<span className="gap">/</span>
|
||||
<span>{total}</span>
|
||||
<Icon
|
||||
className="icon"
|
||||
icon="next"
|
||||
onClick={() => handleChangePage(1)}
|
||||
/>
|
||||
<Icon
|
||||
className="icon"
|
||||
icon="zoom-in"
|
||||
onClick={() => handleChangeScale(1.2)}
|
||||
/>
|
||||
<Icon
|
||||
className="icon"
|
||||
icon="zoom-out"
|
||||
onClick={() => handleChangeScale(0.8)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cx(className, 'PdfViewer')}>
|
||||
<div className={cx('PdfViewer-Content', {'is-loaded': loaded})}>
|
||||
<Document
|
||||
file={file}
|
||||
onLoadSuccess={handleLoadSuccess}
|
||||
loading={renderLoading()}
|
||||
>
|
||||
<Page
|
||||
className={cx('PdfViewer-Content-Page')}
|
||||
pageNumber={page}
|
||||
width={width}
|
||||
height={props.height}
|
||||
loading={renderLoading()}
|
||||
noData={<div>No PDF data</div>}
|
||||
scale={scale}
|
||||
renderTextLayer={false}
|
||||
renderAnnotationLayer={false}
|
||||
/>
|
||||
</Document>
|
||||
</div>
|
||||
{loaded ? renderTool() : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default themeable(PdfViewer);
|
@ -3,7 +3,7 @@ import {localeable, LocaleProps} from 'amis-core';
|
||||
import {themeable, ThemeProps} from 'amis-core';
|
||||
import {Icon} from './icons';
|
||||
|
||||
import type {IconCheckedSchema} from 'amis-ui';
|
||||
import type {IconCheckedSchema} from '../index';
|
||||
|
||||
export interface TimelineItemProps {
|
||||
/**
|
||||
|
@ -244,4 +244,4 @@
|
||||
"react-dom": ">=16.8.6"
|
||||
},
|
||||
"gitHead": "37d23b4a8eb1c663bc38e8dd9040889ea1526ec4"
|
||||
}
|
||||
}
|
||||
|
@ -253,6 +253,7 @@ export type SchemaType =
|
||||
| 'input-formula'
|
||||
| 'diff-editor'
|
||||
| 'office-viewer'
|
||||
| 'pdf-viewer'
|
||||
|
||||
// editor 系列
|
||||
| 'editor'
|
||||
|
@ -153,6 +153,7 @@ import './renderers/Password';
|
||||
import './renderers/DateRange';
|
||||
import './renderers/MultilineText';
|
||||
import './renderers/OfficeViewer';
|
||||
import './renderers/PdfViewer';
|
||||
import './renderers/AMIS';
|
||||
|
||||
import './compat';
|
||||
|
189
packages/amis/src/renderers/PdfViewer.tsx
Normal file
189
packages/amis/src/renderers/PdfViewer.tsx
Normal file
@ -0,0 +1,189 @@
|
||||
/**
|
||||
* @file PdfViewer.tsx PDF 预览
|
||||
*
|
||||
* @created: 2024/02/26
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
autobind,
|
||||
getVariable,
|
||||
isApiOutdated,
|
||||
IScopedContext,
|
||||
Renderer,
|
||||
RendererProps,
|
||||
resolveVariableAndFilter,
|
||||
ScopedContext
|
||||
} from 'amis-core';
|
||||
import {BaseSchema} from '../Schema';
|
||||
|
||||
export const PdfView = React.lazy(
|
||||
() => import('amis-ui/lib/components/PdfViewer')
|
||||
);
|
||||
|
||||
export interface PdfViewerSchema extends BaseSchema {
|
||||
type: 'pdf-viewer';
|
||||
/**
|
||||
* 文件地址
|
||||
*/
|
||||
src?: string;
|
||||
/**
|
||||
* 文件取值,一般配个表单使用
|
||||
*/
|
||||
name?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
background?: string;
|
||||
}
|
||||
|
||||
export interface PdfViewerProps extends RendererProps {}
|
||||
|
||||
interface PdfViewerState {
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export default class PdfViewer extends React.Component<
|
||||
PdfViewerProps,
|
||||
PdfViewerState
|
||||
> {
|
||||
file?: ArrayBuffer;
|
||||
reader?: FileReader;
|
||||
fetchCancel?: Function;
|
||||
constructor(props: PdfViewerProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
loading: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.renderPdf();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: PdfViewerProps) {
|
||||
const props = this.props;
|
||||
|
||||
if (isApiOutdated(prevProps.src, props.src, prevProps.data, props.data)) {
|
||||
this.abortLoad();
|
||||
this.fetchPdf();
|
||||
}
|
||||
|
||||
if (getVariable(props.data, props.name)) {
|
||||
if (
|
||||
getVariable(prevProps.data, prevProps.name) !==
|
||||
getVariable(props.data, props.name)
|
||||
) {
|
||||
this.abortLoad();
|
||||
this.renderPdf();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
abortLoad() {
|
||||
if (this.fetchCancel) {
|
||||
this.fetchCancel('load canceled');
|
||||
this.fetchCancel = undefined;
|
||||
}
|
||||
if (this.reader) {
|
||||
this.reader.abort();
|
||||
this.reader = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
async renderPdf() {
|
||||
const {src, name, data} = this.props;
|
||||
// src 优先级高于 name
|
||||
if (src) {
|
||||
if (!this.file) {
|
||||
await this.fetchPdf();
|
||||
}
|
||||
} else if (getVariable(data, name)) {
|
||||
await this.renderFormFile();
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
async fetchPdf() {
|
||||
const {env, src, data, translate: __} = this.props;
|
||||
const finalSrc = src
|
||||
? resolveVariableAndFilter(src, data, '| raw')
|
||||
: undefined;
|
||||
|
||||
if (!finalSrc) {
|
||||
console.warn('file src is empty');
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
loading: true
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await env.fetcher(finalSrc, data, {
|
||||
responseType: 'arraybuffer',
|
||||
cancelExecutor: (executor: Function) => (this.fetchCancel = executor)
|
||||
});
|
||||
this.file = res.data;
|
||||
this.forceUpdate();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
async renderFormFile() {
|
||||
const {name, data} = this.props;
|
||||
const file = getVariable(data, name);
|
||||
if (file instanceof File) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = _e => {
|
||||
const data = reader.result as ArrayBuffer;
|
||||
this.file = data;
|
||||
this.forceUpdate();
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
this.reader = reader;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {className, classnames: cx, width, height, background} = this.props;
|
||||
|
||||
return (
|
||||
<PdfView
|
||||
file={this.file}
|
||||
className={className}
|
||||
classnames={cx}
|
||||
width={width}
|
||||
height={height}
|
||||
background={background}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Renderer({
|
||||
type: 'pdf-viewer'
|
||||
})
|
||||
export class PdfViewerRenderer extends PdfViewer {
|
||||
static contextType = ScopedContext;
|
||||
|
||||
constructor(props: PdfViewerProps, context: IScopedContext) {
|
||||
super(props);
|
||||
|
||||
const scoped = context;
|
||||
scoped.registerComponent(this);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
super.componentWillUnmount?.();
|
||||
const scoped = this.context as IScopedContext;
|
||||
scoped.unRegisterComponent(this);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user