需求描述
综合会议模块下,所有类型的会议记录都需要添加一个打印功能。每种会议均对应两种模板(如下图),打印相应的内容。
实现思路
将模板当作react组件进行存放。打印时,实时创建iframe作为打印区域,根据参数将对应的模板组件动态引入iframe,实现模板打印。先看下最终效果:
功能实现
1. 按规则对模板命名
我的命名规则:template_模板类型_会议类型
这里我放在同级文件夹下,若模板较多,也可按模板类型分别存放
2. 实现模板内容,并将模板作为react组件导出
// 最终模板内容+++++++
import React from "react";
const Template1_1 = ({record}) => {
// table内容需嵌套一层tbody,否则会报警告
// 此处为部分代码,替换成自己的模板即可
return (
<div>
<table className={'printContent'} border="" cellSpacing="" cellPadding="10">
<tbody>
<tr className={'height50'}>
<th className={'tableName'} colSpan="4">支部党员大会</th>
</tr>
<tr className={'height50'}>
<th className={'tableName'}>会议主题:</th>
<th colSpan="3">{record.theme}</th>
</tr>
</tbody>
</table>
<div className={'qianzi'}>
<div>党支部书记签字:</div>
<div> 年 月 日</div>
</div>
</div>
)
}
export default Template1_1
3. 借助ReactDOM渲染模板
// 引入模板
import TemplateTest from '../component/PrintTemplates/plan/template1_1'
// 此方法是选择模板类型后调用
const print = () => {
const iframe = document.createElement('IFRAME');
iframe.setAttribute('style', 'position:absolute;width:0px;height:0px;left:500px;top:500px;');
document.body.appendChild(iframe);
let doc = iframe.contentWindow.document;
ReactDOM.render(<TemplateTest record={{...record.info, ...record.record}} />, doc)
doc.close();
iframe.contentWindow.focus();
iframe.contentWindow.print();
if (navigator.userAgent.indexOf("MSIE") > 0)
{
document.body.removeChild(iframe);
}
}
至此,打印功能基本实现。
但是有两个问题:
- 样式不是我们理想的,并且尝试了好多种方式均未生效,只有行内样式可以。但一共18个模板,样式大差不差,每一个模板去添加行内样式不可行。页眉和页脚也是不需要的,希望默认取消。
- 以上内容是固定引入了一个模板,但需求要根据会议类型和模板类型动态去引入对应的模板。模板全部引入,用条件判断来区分固然可以,但不利于后期模板的改动
问题解决
1. 样式处理
尝试了多种引入的方式都不生效。尝试将css和模板内容一起返回,果然生效了。
import React from "react";
const Template1_1 = ({record}) => {
return (
<html>
<head>
<style>
.printContent {
width: 100%;
border-collapse:collapse;
font-size: 14px !important;
}
.height50 {
height: 80px;
}
.height200{
height: 200px;
}
.height300 {
height: 300px;
}
.tableName {
width: 15%;
text-align: center;
}
.width12 {
width: 12%;
text-align: center;
}
.qianzi{
text-align: right;
margin-top: 20px;
margin-right: 100px;
}
</style>
</head>
<body>
<div>
<table className={'printContent'} border="" cellSpacing="" cellPadding="10">
<tbody>
<tr className={'height50'}>
<th className={'tableName'} colSpan="4">支部党员大会</th>
</tr>
<tr className={'height50'}>
<th className={'tableName'}>会议主题:</th>
<th colSpan="3">{record.theme}</th>
</tr>
</tbody>
</table>
<div className={'qianzi'}>
<div>党支部书记签字:</div>
<div> 年 月 日</div>
</div>
</div>
</body>
</html>
)
}
export default Template1_1
每一个模板都需要以上的样式解决方案,需将css抽离再引入。但用link引入css文件发现样式又失效了。调试发现,用js的方式引入可行。
样式页
// style.js
// 将样式作为字符串导出
export const styleTemplate = `
.printContent {
width: 100%;
border-collapse:collapse;
font-size: 14px !important;
}
// ... 此处省略,与上方样式相同
// 去除页眉页脚
@page {
margin: 0;
}
// 页面四周留出1cm的空白边
body {
margin: 1cm;
}
`
模板页
import React from "react";
import {styleTemplate} from "@/pages/branch/OrganizationLife/component/PrintTemplates/styleCss"
const Template1_1 = ({record}) => {
return (
<html>
<head>
<style>
{styleTemplate}
</style>
</head>
<body>
<div>
<table className={'printContent'} border="" cellSpacing="" cellPadding="10">
// 模板表格内容
</table>
<div className={'qianzi'}>
<div>党支部书记签字:</div>
<div> 年 月 日</div>
</div>
</div>
</body>
</html>
)
}
export default Template1_1
2. 动态引入模板
每一个模板都需要添加相同的样式,所以需要给所有模板套一个公共的外壳。
所有模板单个引入总觉得有点不太聪明的样子,也考虑到后期的维护,尝试用nodejs的require.context来实现批量引入模板,并动态传入
// 动态模板最终代码
// 将公共的外壳当作一个方法导出
import React from "react";
import {styleTemplate} from "@/pages/branch/OrganizationLife/component/PrintTemplates/styleCss";
const TemplatePrint = async ({meetingType, planType, record}) => {
const templates = require.context('./plan', true, /\.tsx$/)
let components = {};
templates.keys().forEach(fileName => {
let names = fileName.split("/").pop().replace(/\.\w+$/, "");
const componentConfig = templates(fileName);
components[names] = componentConfig.default || componentConfig;
})
const CurTem = await components[`template${planType}_${meetingType}`]
const CurTe = CurTem.default || CurTem
return (
<html>
<head>
<style>
{styleTemplate}
</style>
</head>
<body>
<CurTe record={{...record.info, ...record.record}} />
</body>
</html>
)
}
export default TemplatePrint
// 打印事件最终代码
// 打印方法拿到导出的动态模板
const print = async () => {
const Res11 = await printFun({meetingType, planType: value, record})
const iframe = document.createElement('IFRAME');
iframe.setAttribute('style', 'position:absolute;width:0px;height:0px;left:500px;top:500px;');
document.body.appendChild(iframe);
let doc = iframe.contentWindow.document;
ReactDOM.render(Res11, doc)
doc.close();
iframe.contentWindow.focus();
iframe.contentWindow.print();
if (navigator.userAgent.indexOf("MSIE") > 0)
{
document.body.removeChild(iframe);
}
}
错误总结
以上就是全部内容了,但是在开发过程中,还遇到了很多问题。这里总结一下 ,希望能有帮助。
- 为何TemplatePrint作为方法返回?
根据TemplatePrint的内容应该能看出来,我一开始是想以组件的形式导出,在ReactDOM.render方法中直接渲染组件。
打印components发现,require.context拿到的数据整理后的数据是一个值为promise的集合,要想同步的使用模板里的内容,需要添加async-await。添加之后还用组建形式去渲染,会出现以下报错:
所以这里当作普通方法,将组建内容返回。
需要注意的是,作为普通方法的动态模板,返回值也是一个promise对象,调用时也需要async-await来获取到内容,打印输出内容为动态模板的虚拟dom就成功了
- 开发环境与生产环境的差异
const CurTem = await components[`template${planType}_${meetingType}`]
const CurTe = CurTem.default || CurTem
在开发环境中,模板组件内容在await拿到的promise返回值default中,
在生产环境中,模板组件内容就是await拿到的promise的返回值。若使用default,会出现以下报错信息:
Uncaught Error: Minified React error #130; visit http://reactjs.org/docs/error-decoder.html?invariant=130&args[]=object&args[]= for the full message or use the non-minified dev environment for full errors and additional helpful warnings.