前端实现PDF预览下载打印
Contents
为了实现前端预览pdf文件,在vue3和react中都进行了尝试
React
react-pdf
在react中用到的组件是react-pdf,我们可以对组件进行二次封装来使组件支持放大,缩小,旋转,下载等功能
import { useState } from 'react';
import './styles.css';
import { Document, Page, pdfjs } from "react-pdf";
import { Spin, Watermark } from 'antd';
import {
LeftCircleOutlined,
RightCircleOutlined,
ZoomInOutlined,
ZoomOutOutlined,
CompressOutlined,
ExpandOutlined,
CloseOutlined,
LoadingOutlined,
DownloadOutlined,
RedoOutlined
} from '@ant-design/icons'
console.log('pdfjs.version', pdfjs.version);
pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`;
type OptionType = {
show: boolean;
url: string;
}
type SetOptionType = React.Dispatch<React.SetStateAction<OptionType>>
type Props = {
option: OptionType,
setOption: SetOptionType,
WatermarkCon?: string,
downLoadHandle?: () => void
httpHeaders?: object | null | undefined;
withCredentials?: boolean | null | undefined;
}
type AngleType = 0 | 90 | 180 | 270
const antIcon = <LoadingOutlined style={{ fontSize: 24, color: 'white' }} spin />;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function throttle<T extends (...args: any[]) => any>(func: T, wait: number = 400): T {
let timeout: ReturnType<typeof setTimeout> | null = null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let context: any;
let args: IArguments | null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const throttled = function (this: any) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
context = this;
// eslint-disable-next-line prefer-rest-params
args = arguments;
if (!timeout) {
timeout = setTimeout(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
func.apply(context, args as any);
timeout = null;
}, wait);
}
};
return throttled as T;
}
const PdfPreview = ({ option, setOption, downLoadHandle, WatermarkCon, httpHeaders, withCredentials }: Props) => {
const [pageNumber, setPageNumber] = useState(1)
const [pageNumberInput, setPageNumberInput] = useState(1)
const [pageNumberFocus, setPageNumberFocus] = useState(false)
const [numPages, setNumPages] = useState(1)
const [pageWidth, setPageWidth] = useState(503)
const [fullscreen, setFullscreen] = useState(false)
const [angle, setAngle] = useState<AngleType>(0)
const cleanUp = () => {
setPageNumber(1)
setPageNumberInput(1)
setPageNumberFocus(false)
setNumPages(1)
setPageWidth(503)
setFullscreen(false)
setAngle(0)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const onDocumentLoadSuccess = (data: any) => {
const totalPages = data['_transport']['_numPages'] || 1
setNumPages(totalPages)
}
const rotatePage = () => {
setAngle((oldAngle) => {
switch (oldAngle) {
case 0:
return 90
case 90:
return 180
case 180:
return 270
case 270:
return 0
}
})
}
const downLoad = () => {
downLoadHandle && downLoadHandle()
}
const lastPage = () => {
if (pageNumber === 1) return;
const page = pageNumber - 1;
setPageNumber(page)
setPageNumberInput(page)
}
const nextPage = () => {
if (pageNumber === numPages) return;
const page = pageNumber + 1;
setPageNumber(page)
setPageNumberInput(page)
}
const onPageNumberFocus = () => {
setPageNumberFocus(true)
}
const onPageNumberBlur = () => {
setPageNumberFocus(false)
setPageNumberInput(pageNumber)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const onPageNumberChange = (e: any) => {
let value = Number(e.target.value);
value = value <= 0 ? 1 : value;
value = value >= numPages ? numPages : value;
setPageNumber(value)
setPageNumberInput(value)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const toPage = (e: any) => {
if (e.keyCode === 13) {
setPageNumber(Number(e.target.value))
}
}
const pageZoomOut = () => {
if (pageWidth <= 503) return;
setPageWidth((oldv) => oldv * 0.8)
}
const pageZoomIn = () => {
setPageWidth((oldv) => oldv * 1.2)
}
const pageFullscreen = () => {
if (fullscreen) {
setFullscreen(false)
setPageWidth(600)
} else {
setFullscreen(true)
setPageWidth(window.screen.width - 40)
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const closeViewer = (e: any, model?: 'pdf-view') => {
if (e) {
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
}
const initOption: OptionType = {
show: false,
url: ''
}
if (e.target?.className === model || !model) {
setOption && setOption(initOption)
cleanUp()
}
}
return (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
<div className={option.show ? 'show' : 'notShow'} onClick={(e: any) => closeViewer(e, 'pdf-view')} >
<div className={'mask-close'} onClick={(e) => closeViewer(e)}>
<CloseOutlined style={{}} />
</div>
<div className="pdf-view">
<div className="container">
<Watermark content={WatermarkCon ? WatermarkCon : 'Watermark'}>
<Document
key={option.url}
className={'view-dom'}
noData={<h2 className='top300'>无pdf数据...</h2>}
loading={<Spin className='top300' indicator={antIcon} />}
file={option.url}
options={{
httpHeaders: httpHeaders || null,
withCredentials: withCredentials || null
}}
onLoadSuccess={(data) => onDocumentLoadSuccess(data)}
>
<Page
key={pageNumber}
rotate={angle}
width={pageWidth}
renderTextLayer={false}
pageNumber={pageNumber}
renderAnnotationLayer={false}
onLoadSuccess={(data) => onDocumentLoadSuccess(data)}
/>
</Document>
</Watermark>
</div>
<div className="page-tool">
{downLoadHandle && <div className="page-tool-item" onClick={throttle(downLoad)}>
<DownloadOutlined style={{}} />
</div>}
<div className="page-tool-item" onClick={throttle(rotatePage)}>
{/* 旋转 */}
<RedoOutlined style={{}} />
</div>
<div className="page-tool-item" onClick={throttle(lastPage)}>
{/* 上一页 */}
<LeftCircleOutlined style={{}} />
</div>
<div className="page-tool-item" onClick={throttle(nextPage)}>
{/* 下一页 */}
<RightCircleOutlined style={{}} />
</div>
<div className="input">
<input
value={pageNumberFocus ? pageNumberInput : pageNumber}
onFocus={onPageNumberFocus}
onBlur={onPageNumberBlur}
onChange={(e) => onPageNumberChange(e)}
onKeyDown={toPage}
type="number"
/>
/ {numPages}
</div>
<div className="page-tool-item" onClick={throttle(pageZoomIn)}>
{/* 放大 */}
<ZoomInOutlined style={{}} />
</div>
<div className="page-tool-item" onClick={throttle(pageZoomOut)}>
{/* 缩小 */}
<ZoomOutOutlined style={{}} />
</div>
<div className="page-tool-item" onClick={throttle(pageFullscreen)}>
{/* {fullscreen ? "恢复默认" : "适合窗口"} */}
{fullscreen ? <CompressOutlined style={{}} /> : <ExpandOutlined style={{}} />}
</div>
</div>
</div>
</div >
);
};
export default PdfPreview;
引入
import { useState } from 'react'
import { Button } from 'antd'
import { PdfPreview as PDFViwer } from '../components'
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
const PdfPreview = (props: any) => {
const [pdfOption, setPdfOption] = useState({
show: false,
url: ''
})
return (
<>
<div style={{
width: 400,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
margin: '20px auto',
}}>
<PDFViwer
option={pdfOption}
setOption={setPdfOption}
downLoadHandle={() => {
console.log('downLoadHandle');
}}
/>
</div>
</>
)
}
export default PdfPreview
react-pdf的使用参考了掘金博主一颗Potato的文章,并在此基础上进一步做了改进
vue3
在vue3中尝试用过了两个组件
因为没有找到显示签名的具体解决办法,部分签名仍未显示,所以最后选择了pdfjs
vue3-pdf-app
vue3-pdf-app对vue-pdf-app做了vue3的兼容,是一个基于Vue 3和PDF.js的PDF阅读器组件,可以方便地在Vue应用程序中嵌入PDF文件并预览。
在vue3-pdf-app如果要预览pdf签名的话需要对下列代码进行注释
if (data.fieldType === "Sig") {
data.fieldValue = null;
// this.setFlags(_util.AnnotationFlag.HIDDEN); // 全局搜索_util.AnnotationFlag.HIDDEN 注释相关行代码
}
pdfjs
https://gitcode.gitcode.host/docs-cn/pdf.js-docs-cn/getting_started/index.html
PDF.js是Mozilla开发的一个JavaScript库,用于在Web浏览器中解析和显示PDF文档。它允许开发人员将PDF文件嵌入到网页中,并使用JavaScript来控制PDF的呈现和交互。PDF.js的主要优点是它可以在任何支持JavaScript的浏览器中运行,不需要任何插件或下载。
我们可以使用pdfjs打包好的文件,通过iframe的方式来进行pdf的预览
使用pdfjs的原因是pdfjs打包后的文件可以完美的展示pdf签名,
进行简单封装
<script setup lang='ts'>
import { onMounted, ref } from 'vue';
const props = withDefaults(
defineProps<{
width: string,
height: string,
src: string | BlobPart
}>(),
{
width: "100%",
height: "100%",
src: ''
}
)
const pdfUrl = ref<string>()
onMounted(() => {
if (typeof props.src === 'string') {
pdfUrl.value = props.src
} else {
pdfUrl.value = window.URL.createObjectURL(new Blob([props.src]))
}
})
</script>
<template>
<iframe v-bind="props" :src="`./pdfjs/web/viewer.html?file=${props.src}`" frameborder="0"></iframe>
</template>
<style scoped lang='less'></style>
对于传入流式二进制文件将其转化为blob地址进行预览
在线预览地址: https://pdf.plumliil.cn/