前端React实现pdf在线阅读和电子合同

前端React实现pdf在线阅读和电子合同

介绍

前段时间看到网上有个趋势,今后合同也开始变成无纸化,如果要签约,只需要在网上签名,不需要本人去现场。之前没有做过,决定试试看效果。

1. 搭建页面

因为是我自建的脚手架,所以暂时先搭建个骨架用来做测试页面的开发。

提供一个简单的搭建步骤:

  1. npx create-react-app [name] --template typescript

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zEGRKtws-1644143802262)(./ReadMe.assets/image-20220206175120080.png)]

搭建好页面骨架后,我在前端周报上面翻了翻,找到了2个不错的库: js-pdfreact-pdf

  • jsPdf: 前端生成pdf文件。支持直接下载、转成data url
  • react-pdf: 实时展示pdf内容。支持打开文件和打开链接。
    • 注意: 如果使用webpack5的话,不要使用国内的npm源,一定要用国外最新版本的,否则会出现dev-server崩溃栈溢出的问题。

思路

如果要实现pdf版本的合同,我个人分为4步:

  1. 读取、展示合同内容。
  2. 制作合同。
  3. 制作合同的同时,能实时看到内容。
  4. 下载文件。

后期扩展:
5. 线上签名。
6. 盖章留戳。

1. 读取、展示合同内容

利用react-pdf的展示和读取功能,实现pdf文件的展示。首先,先不考虑文件的来源,我们以本地的测试pdf为准。

在这里插入图片描述

在开发页面引入react-pdf,下面是实现源码

import React, { useState } from 'react';
import { Document, Page } from 'react-pdf/dist/esm/entry.webpack';
// 引入样式,修复不对齐的问题
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';

import sample from './test.pdf';

const PDFDisplay = () => {
      const [totalPages, setTotalPages] = useState<number>(0)
    	const [pageNumber, setPageNumber] = useState<number>(1)
      const [file, setFile] = useState(sample)

      return (
        <>
                <Card style = {{
            margin: 8,
        }}>
            <Title level={3}>
                React-pdf 查看/显示 pdf内容
            </Title>

            <div style = {{
                margin: 8,
            }}>
                
            <div style = {{
                margin: 6,
                display: 'flex',
                flexDirection: 'row-reverse',
            }}>
            <div className="Example__container__load">
                <input onChange={onFileChange} type="file" />
            </div>
                <Button 
            style={{
                margin: 4,
            }}
            type='primary'
            onClick={turnHeadPages}
            >查看首页</Button>
            <Button 
            style={{
                margin: 4,
            }}
            type='primary'
            onClick={turnTailPages}
            >查看尾页</Button>
            <Button 
            style={{
                margin: 4,
            }}
            type='primary'
            onClick={handleReducePages}
            >上一页</Button>
            <Button 
            style={{
                margin: 4,
            }}
            type='primary'
            onClick={handleTurnPages}
            >下一页</Button>
            </div>

            <div style={{
                display: 'flex',
                justifyContent: 'center',
            }}>
            <Document file={file} 
                onLoadSuccess={(values: any) => setTotalPages(values?.numPages)} options={options} >
                <Page pageNumber={pageNumber} renderMode='svg' />
            </Document>
            </div>
            </div>

            <div style={{
                margin: 6,
                display: 'flex',
                flexDirection: 'row-reverse',
            }}>第{pageNumber}页, 共{totalPages}页</div>
        </Card>
        </>
      )
  
  }

export default PDFDisplay

可以看到文件已经被读取出来:

在这里插入图片描述

在这里插入图片描述

因为react-pdf生成了一个隐藏的dom,所以在复制文字的时候,能看到和展示的内容又区别,甚至出现了一些位置上的偏移。

在这里插入图片描述

为了解决这种不对齐的问题,需要引入样式,最大限度的避免这种样式偏移:

// 引入样式,修复不对齐的问题
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';

因为pdf是逐页读取的,我并没有采用官方doc的做法把所有的内容全部展示出来,因为需要写上4个按钮,实现灵活操作。

在这里插入图片描述

这个是按钮的方法:


    // 翻下页
    const handleTurnPages = () => {
        if (pageNumber < totalPages) setPageNumber(pageNumber + 1)
        else message.error('已经到最后一页!')
    }

    // 翻上页
    const handleReducePages = () => {
        if (pageNumber > 1) setPageNumber(pageNumber - 1)
        else message.error('已经到首页!')
    }

    // 回到首页
    const turnHeadPages = () => {
        if (pageNumber === 1) message.error('已经是首页')
        else setPageNumber(1)
    }

    // 回到尾页
    const turnTailPages = () => {
        if (pageNumber === totalPages) message.error('已经是尾页')
        else setPageNumber(totalPages)
    }

    // 上传文件
    const onFileChange = (event: any) => {
        const file = event?.target.files[0]
        const isPDF = file?.type === 'application/pdf'

        if(!isPDF) {
            message.error(`${file.name}不是一个pdf文件!`)
            return
        }
        console.log('event:', event.target.files);
        
        setFile(file)
    }

在展示过程中,如果我们的主要内容是汉字,会出现乱码情况。而汉字属于utf-8,我们需要额外引入一些东西,并且对webpack.config进行修改:

// 非拉丁语的格式问题修复
    // 此处需要修改webpack.config, CopyWebpackPlugin
    const options = {
        cMapUrl: 'cmaps/',
        cMapPacked: true,
    };

安装新的plugin:

npm i copy-webpack-plugin -S

打开webpack.config的配置文件,写入以下代码:

const CopyWebpackPlugin = require('copy-webpack-plugin');

new CopyWebpackPlugin({
  patterns: [
    {
      from: path.join(path.dirname(require.resolve('pdfjs-dist/package.json')), 'cmaps'),
      to: 'cmaps/' 
    },
  ],
}),

解决了汉字显示问题:

在这里插入图片描述

附录

1. jsPDF

  • 文档说明比较少,需要仔细阅读源码
  • 暂时不支持utf-8
    • 如果要保存为图片,需要使用html2canvas来使用, html2canvas
    • 更换为支持utf-8pdfmake或者pdfkit

实现

1. 下载pdf文件

  • 指定内容下载
    • pdf.save()
    • pdf.output()
  • canvas展示效果,并转换下载

2. 显示pdf文件

  • 本地文件展示
  • 实时下载结果展示