背景
前端干了6年,迷迷糊糊的,突然想写写博客,把自己这六年来,攻克的技术难点,功能点记录一下,写的不好,大家体谅一下。
方法
下面是,我在OA系统做的一个功能点,做了一个前端统一打印功能,主要是OA系统: 先说一下目前主流的实现打印的实现方式吧:
- 后端实现:提前写好word模板,后台把数据塞进去,然后前端再调用接口,下载文件,下载后再打印。(或者是浏览器机制。是PDF的话,下载后直接有预览,预览里面有下载。)
- 前端实现一:将将页面截图,塞进word里面。直接下载,或者转pdf下载。
- 前端实现二:前端请接口,准备word模版里面,把数据塞进去。
弊端
先说上面实现的几个弊端吧:
- 我不是不想用1方法,采用1没有去前端什么事,调个下载接口就行了,可是太多流程了打印,后端都要发癫了,不给我干这活哦,少量打印还好。
- 2方法简单了,做一个统一的方法,将页面截图,塞进word里面,但是经不住他丑呀……截图系统的图,塞进word里会变形…表单一长,就显示不全。
- 3方法也是如此,要对接太多接口了,准备太多太多模版了,前端我自己也干不来。
也许有更好的,我技术有限,没有想到。我用的是都不是这三种,我先上截图吧,不然你们不信,我那还真有这场景,100多个表单打印。
效果图
流程界面图
点击打印按钮,进行打印预览
其他流程打印图
一百多个流程表单
真的有很多打印………………………………
上代码(哈哈,上菜)
所用插件
canvas-editor
基于canvas/svg的富文本编辑器 文档地址
canvas-editor, 它底层基于 canvas 实现, 我们使用它可以实现类似于 word文档编辑器类似的效果, 同时还支持很多灵活可配置的 API, 可以帮助我们定制属于自己的文档编辑平台。
我为什么要用这个插件呢,因为这个插件是现成的,而且样式好看,开源的。(感恩canvas-editor);很符合是目前的项目要求,用这个插件,我只需要封装一个方法,把我界面的元素和数据,解析成canvas-editor构成word,所需要的数据格式就可以进行预览,且打印就可以了。
实现思路
由于业务需要,打印一共分两个,一个是表单模块,一个是意见模块。由于表单是动态的,数据字段及属性格式是不固定的,而意见模块的数据格式是固定。
1、封装vue组件弹框
<template>
<el-dialog v-model="dialogVisible" title="表单打印" :width="980" :close-on-click-modal="false"
:close-on-press-escape="false" :destroy-on-close="true" draggable>
<!-- 预览容器--->
<div
style="max-height: calc(100vh - 200px);background: #fafafa;text-align: center;overflow: auto;display: flex;justify-content: center;">
<div class="canvas-editor"></div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="printfFormFn()">打印</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup name="printfForm">
const dialogVisible=ref(false)
// 打开弹框
const openDialog = (domHtml: HTMLElement,commentList: any[],) => {
// domHtmlm 是表单的所有元素节点
// commentList 是意见列表数据格式的[{node:'流程节点',state:'状态',userName:'审批人',time:'审批 时间',comment:'审批意见'}]
// 初始化canvas-editor
initCanvasEditor(domHtml,commentList)
dialogVisible.value = true;
}
defineExpose({
openDialog
})
</script>
对外抛出打开预览组件方法
2、初始化canvas-editor
进入插件canvas-editor
import docxPlugin from "@hufe921/canvas-editor-plugin-docx";
封装初始化方法
import docxPlugin from "@hufe921/canvas-editor-plugin-docx";
// 将表格行分未24格子,每格长28像素
const colgroupList: { width: number }[] = []
for (let i = 0; i < 24; i++) {
colgroupList.push({ width: 28 })
}
// 容器对象
let instance=null
const initWord = (domHtml: HTMLElement, commentList: any[]) => {
// 获取标题
const title = initTittle(domHtml)
//处理好的意见数据,对应canvas-editor是行数据
const commentListWord: ITr[] = initCommentList(JSON.parse(JSON.stringify(commentList)))
//处理好的格式数据,对应canvas-editor是行数据
const formListWord: ITr[] = initFormList(domHtml)
// 获取容器
let dom = document.querySelector('.canvas-editor') as HTMLDivElement
// 初始化容器
if (dom) {
instance = new Editor(
dom,
[
{// 标题设置
value: title + '\n',
size: 24,
rowFlex: RowFlex.CENTER,
bold: true,
rowMargin: 0,
},
{// 意见设置
value: '',
colgroup: colgroupList,
type: ElementType.TABLE,
trList: formList,
},
{ //表单设置
value: '',
colgroup: colgroupList,
type: ElementType.TABLE,
trList: commentList,
}
],
{
mode: EditorMode.READONLY,
watermark: {// 水印设置
data: 'XXX公司 [' + userName + ' ' + formatDate(new Date()) + ' 打印]',
data: '',
size: 14,
opacity: 0.2,
repeat: true
},
margins: [50, 60, 50, 60] as unknown as IMargin,// 纸张设置
}
)
}
}
3、初始化标题
const initTittle = (domHtml: HTMLElement) => {
let title = ''
let dom: HTMLElement = domHtml.querySelector('.title') as HTMLElement
if (dom) {
title = dom.innerText
}
if(!title){
title = '未知标题'
}
return title
}
4、初始化意见行列表
// 初始化意见行列
const initCommentList = (commentList:any[]) => {
// 处理审核意见
const list: ITr[] = []
// 初始化表头
list.push({
height: 30,
tdList: [
{
colspan: 4,// 占四列
rowspan: 1,//占一行
backgroundColor: '#fafafa',
value: [
{ value: `环节`, size: 16, rowFlex: RowFlex.CENTER },
]
},
{
colspan: 3,
rowspan: 1,
backgroundColor: '#fafafa',
value: [
{ value: `状态`, size: 16, rowFlex: RowFlex.CENTER },
]
},
{
colspan: 8,
rowspan: 1,
backgroundColor: '#fafafa',
value: [
{ value: `审批意见`, size: 16, rowFlex: RowFlex.CENTER },
]
},
{
colspan: 3,
rowspan: 1,
backgroundColor: '#fafafa',
value: [
{ value: `审批人`, size: 16, rowFlex: RowFlex.CENTER },
]
},
{
colspan: 6,
rowspan: 1,
backgroundColor: '#fafafa',
value: [
{ value: `审批时间`, size: 16, rowFlex: RowFlex.CENTER },
]
},
]
})
commentList.forEach(((item: any) => {
let row = {
height: 30,
tdList: [
{
colspan: 4,// 占四列
rowspan: 1,//占一行
backgroundColor: '#fafafa',
value: [
{ value: item.node, size: 16, rowFlex: RowFlex.CENTER },
]
},
{
colspan: 3,
rowspan: 1,
value: [
{ value: item.state, size: 16, rowFlex: RowFlex.CENTER },
]
},
{
colspan: 8,
rowspan: 1,
value: [
{ value: item.comments, size: 16, rowFlex: RowFlex.CENTER },
]
},
{
colspan: 3,
rowspan: 1,
value: [
{ value: item.userName, size: 16, rowFlex: RowFlex.CENTER },
]
},
{
colspan: 6,
rowspan: 1,
value: [
{ value: item.time, size: 16, rowFlex: RowFlex.CENTER },
]
},
]
}
list.push(row)
})
return list
}
5、处理表单节点,获取到对应的行数据
const initFormList=(domHtml: HTMLElement)=>{
//定义返回结果行
const resultList: ITr[] = []
// 定义标题对象
const titleRow: ITr = {
height: 30,
tdList: [
{
colspan: 24,
rowspan: 1,
verticalAlign: VerticalAlign.MIDDLE,
value: [
{ value: '基本信息', size: 15, rowFlex: RowFlex.LEFT, bold: true },
]
},
]
}
// 定义标签对象
const labelTd: ITd = {
colspan: 4,
rowspan: 1,
verticalAlign: VerticalAlign.MIDDLE,
backgroundColor: '#fafafa',
value: [
{ value: '标题', size: 14, rowFlex: RowFlex.CENTER },
]
}
// 定义值对象
const valueTd: ITd = {
colspan: 8,
rowspan: 1,
verticalAlign: VerticalAlign.MIDDLE,
value: [
{ value: '值', size: 14, rowFlex: RowFlex.LEFT },
]
}
// 定义行对象
const valRow: ITr = {
height: 30,
tdList: []
}
//表单设置
let pageFormList = domHtml.querySelectorAll('.el-form') as unknown as HTMLElement[]
if (pageFormList.length > 0) {
pageFormList.forEach(pageForm => {
let childNodes = pageForm.childNodes as NodeListOf<HTMLElement>; // 获取所有的子节点
childNodes.forEach((node: HTMLElement) => {
// 处理二级标题
if (node.className && node.className == 'subTitle') {
if (valRow && valRow.tdList.length == 2) {
valRow.tdList[1].colspan = 20;
resultList.push(JSON.parse(JSON.stringify(valRow)))
valRow.tdList = [];
} else if (valRow && valRow.tdList.length == 4) {
resultList.push(JSON.parse(JSON.stringify(valRow)))
valRow.tdList = [];
}
titleRow.tdList[0].value[0].value = node.innerText
resultList.push(JSON.parse(JSON.stringify(titleRow)))
}
else if (node.classList && node.classList.contains('el-form-item')) {
const dom1 = node.querySelector('.el-form-item__label') as HTMLElement
if (dom1) {
const dom2 = node.querySelector('.el-form-item__content') as HTMLElement
let value = dom2.innerText || ''
// 处理输入框的值
const inputDom = dom2.querySelector('input')
if (inputDom) {
value = inputDom.value
}
//处理文本输入框的值
const textareaDom = dom2.querySelector('textarea')
if (textareaDom) {
value = textareaDom.value
}
// 获取单选款选中值
const radioDom = dom2.querySelector('.el-radio-group') as HTMLElement
if (radioDom) {
const valDom = radioDom.querySelector('.is-checked') as HTMLElement
if (valDom) {
value = valDom.innerText || ''
} else {
value = ''
}
}
//处理多选框的值
const selectDom = dom2.querySelector('.el-select') as HTMLElement
if (selectDom) {
const valDom = selectDom.querySelector('.el-select__tags') as HTMLElement
if (valDom) {
value = valDom.innerText || ''
}
}
const listValueTd: string[] = []
// 处理附件样式
const documentDomList = dom2.querySelectorAll('.el-icon-document') as unknown as HTMLElement[]
if (documentDomList.length > 0) {
let listValue: string[] = []
documentDomList.forEach((fileDom: any) => {
listValueTd.push(fileDom.innerText)
listValue.push(fileDom.innerText)
})
value = listValue.join('\n')
}
let titleVal = dom1.innerText
labelTd.value[0].value = titleVal || ''
valueTd.value[0].value = value || ''
//处理每行的逻辑
if (valRow.tdList.length <= 2) {
// 单独占一行的情况1
if (node.classList.contains('lineRow') && valRow.tdList.length == 2) {
valRow.tdList[1].colspan = 20
resultList.push(JSON.parse(JSON.stringify(valRow)))
valRow.tdList = []
valRow.tdList.push(JSON.parse(JSON.stringify(labelTd)))
valRow.tdList.push(JSON.parse(JSON.stringify(valueTd)))
valRow.tdList[1].colspan = 20
resultList.push(JSON.parse(JSON.stringify(valRow)))
valRow.tdList = []
}
//单独占一行的情况2
else if (node.classList.contains("lineRow") && valRow.tdList.length == 0) {
valRow.tdList.push(JSON.parse(JSON.stringify(labelTd)))
valRow.tdList.push(JSON.parse(JSON.stringify(valueTd)))
valRow.tdList[1].colspan = 20
resultList.push(JSON.parse(JSON.stringify(valRow)))
valRow.tdList = []
} else {
valRow.tdList.push(JSON.parse(JSON.stringify(labelTd)))
valRow.tdList.push(JSON.parse(JSON.stringify(valueTd)))
}
} else {
resultList.push(JSON.parse(JSON.stringify(valRow)))
valRow.tdList = [];
valRow.tdList.push(JSON.parse(JSON.stringify(labelTd)))
valRow.tdList.push(JSON.parse(JSON.stringify(valueTd)))
}
}
}
})
})
}
//处理行行数据
if (valRow && valRow.tdList.length == 2) {
valRow.tdList[1].colspan = 20;
resultList.push(JSON.parse(JSON.stringify(valRow)))
valRow.tdList = [];
} else if (valRow && valRow.tdList.length == 4) {
resultList.push(JSON.parse(JSON.stringify(valRow)))
valRow.tdList = [];
}
titleRow.tdList[0].value[0].value = '审批意见'
resultList.push(JSON.parse(JSON.stringify(titleRow)))
return resultList
}
调用打印方法
const printfFormFn = () => {
instance.command.executePrint()
}
完整代码
<template>
<el-dialog v-model="dialogVisible" title="表单打印" :width="980" :close-on-click-modal="false"
:close-on-press-escape="false" :destroy-on-close="true" draggable>
<!-- 预览容器--->
<div
style="max-height: calc(100vh - 200px);background: #fafafa;text-align: center;overflow: auto;display: flex;justify-content: center;">
<div class="canvas-editor"></div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="printfFormFn()">打印</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup name="printfForm">
import docxPlugin from "@hufe921/canvas-editor-plugin-docx";
const dialogVisible=ref(false)
// 打开弹框
const openDialog = (domHtml: HTMLElement,commentList: any[],) => {
// domHtmlm 是表单的所有元素节点
// commentList 是意见列表数据格式的[{node:'流程节点',state:'状态',userName:'审批人',time:'审批 时间',comment:'审批意见'}]
// 初始化canvas-editor
initCanvasEditor(domHtml,commentList)
dialogVisible.value = true;
}
// 将表格行分未24格子,每格长28像素
const colgroupList: { width: number }[] = []
for (let i = 0; i < 24; i++) {
colgroupList.push({ width: 28 })
}
// 容器对象
let instance=null
const initWord = (domHtml: HTMLElement, commentList: any[]) => {
// 获取标题
const title = initTittle(domHtml)
//处理好的意见数据,对应canvas-editor是行数据
const commentListWord: ITr[] = initCommentList(JSON.parse(JSON.stringify(commentList)))
//处理好的格式数据,对应canvas-editor是行数据
const formListWord: ITr[] = initFormList(domHtml)
// 获取容器
let dom = document.querySelector('.canvas-editor') as HTMLDivElement
// 初始化容器
if (dom) {
instance = new Editor(
dom,
[
{// 标题设置
value: title + '\n',
size: 24,
rowFlex: RowFlex.CENTER,
bold: true,
rowMargin: 0,
},
{// 意见设置
value: '',
colgroup: colgroupList,
type: ElementType.TABLE,
trList: formList,
},
{ //表单设置
value: '',
colgroup: colgroupList,
type: ElementType.TABLE,
trList: commentList,
}
],
{
mode: EditorMode.READONLY,
watermark: {// 水印设置
data: 'XXX公司 [' + userName + ' ' + formatDate(new Date()) + ' 打印]',
data: '',
size: 14,
opacity: 0.2,
repeat: true
},
margins: [50, 60, 50, 60] as unknown as IMargin,// 纸张设置
}
)
}
}
const initTittle = (domHtml: HTMLElement) => {
let title = ''
let dom: HTMLElement = domHtml.querySelector('.title') as HTMLElement
if (dom) {
title = dom.innerText
}
if(!title){
title = '未知标题'
}
return title
}
// 初始化意见行列
const initCommentList = (commentList:any[]) => {
// 处理审核意见
const list: ITr[] = []
// 初始化表头
list.push({
height: 30,
tdList: [
{
colspan: 4,// 占四列
rowspan: 1,//占一行
backgroundColor: '#fafafa',
value: [
{ value: `环节`, size: 16, rowFlex: RowFlex.CENTER },
]
},
{
colspan: 3,
rowspan: 1,
backgroundColor: '#fafafa',
value: [
{ value: `状态`, size: 16, rowFlex: RowFlex.CENTER },
]
},
{
colspan: 8,
rowspan: 1,
backgroundColor: '#fafafa',
value: [
{ value: `审批意见`, size: 16, rowFlex: RowFlex.CENTER },
]
},
{
colspan: 3,
rowspan: 1,
backgroundColor: '#fafafa',
value: [
{ value: `审批人`, size: 16, rowFlex: RowFlex.CENTER },
]
},
{
colspan: 6,
rowspan: 1,
backgroundColor: '#fafafa',
value: [
{ value: `审批时间`, size: 16, rowFlex: RowFlex.CENTER },
]
},
]
})
commentList.forEach(((item: any) => {
let row = {
height: 30,
tdList: [
{
colspan: 4,// 占四列
rowspan: 1,//占一行
backgroundColor: '#fafafa',
value: [
{ value: item.node, size: 16, rowFlex: RowFlex.CENTER },
]
},
{
colspan: 3,
rowspan: 1,
value: [
{ value: item.state, size: 16, rowFlex: RowFlex.CENTER },
]
},
{
colspan: 8,
rowspan: 1,
value: [
{ value: item.comments, size: 16, rowFlex: RowFlex.CENTER },
]
},
{
colspan: 3,
rowspan: 1,
value: [
{ value: item.userName, size: 16, rowFlex: RowFlex.CENTER },
]
},
{
colspan: 6,
rowspan: 1,
value: [
{ value: item.time, size: 16, rowFlex: RowFlex.CENTER },
]
},
]
}
list.push(row)
})
return list
}
const initFormList=(domHtml: HTMLElement)=>{
//定义返回结果行
const resultList: ITr[] = []
// 定义标题对象
const titleRow: ITr = {
height: 30,
tdList: [
{
colspan: 24,
rowspan: 1,
verticalAlign: VerticalAlign.MIDDLE,
value: [
{ value: '基本信息', size: 15, rowFlex: RowFlex.LEFT, bold: true },
]
},
]
}
// 定义标签对象
const labelTd: ITd = {
colspan: 4,
rowspan: 1,
verticalAlign: VerticalAlign.MIDDLE,
backgroundColor: '#fafafa',
value: [
{ value: '标题', size: 14, rowFlex: RowFlex.CENTER },
]
}
// 定义值对象
const valueTd: ITd = {
colspan: 8,
rowspan: 1,
verticalAlign: VerticalAlign.MIDDLE,
value: [
{ value: '值', size: 14, rowFlex: RowFlex.LEFT },
]
}
// 定义行对象
const valRow: ITr = {
height: 30,
tdList: []
}
//表单设置
let pageFormList = domHtml.querySelectorAll('.el-form') as unknown as HTMLElement[]
if (pageFormList.length > 0) {
pageFormList.forEach(pageForm => {
let childNodes = pageForm.childNodes as NodeListOf<HTMLElement>; // 获取所有的子节点
childNodes.forEach((node: HTMLElement) => {
// 处理二级标题
if (node.className && node.className == 'subTitle') {
if (valRow && valRow.tdList.length == 2) {
valRow.tdList[1].colspan = 20;
resultList.push(JSON.parse(JSON.stringify(valRow)))
valRow.tdList = [];
} else if (valRow && valRow.tdList.length == 4) {
resultList.push(JSON.parse(JSON.stringify(valRow)))
valRow.tdList = [];
}
titleRow.tdList[0].value[0].value = node.innerText
resultList.push(JSON.parse(JSON.stringify(titleRow)))
}
else if (node.classList && node.classList.contains('el-form-item')) {
const dom1 = node.querySelector('.el-form-item__label') as HTMLElement
if (dom1) {
const dom2 = node.querySelector('.el-form-item__content') as HTMLElement
let value = dom2.innerText || ''
// 处理输入框的值
const inputDom = dom2.querySelector('input')
if (inputDom) {
value = inputDom.value
}
//处理文本输入框的值
const textareaDom = dom2.querySelector('textarea')
if (textareaDom) {
value = textareaDom.value
}
// 获取单选款选中值
const radioDom = dom2.querySelector('.el-radio-group') as HTMLElement
if (radioDom) {
const valDom = radioDom.querySelector('.is-checked') as HTMLElement
if (valDom) {
value = valDom.innerText || ''
} else {
value = ''
}
}
//处理多选框的值
const selectDom = dom2.querySelector('.el-select') as HTMLElement
if (selectDom) {
const valDom = selectDom.querySelector('.el-select__tags') as HTMLElement
if (valDom) {
value = valDom.innerText || ''
}
}
const listValueTd: string[] = []
// 处理附件样式
const documentDomList = dom2.querySelectorAll('.el-icon-document') as unknown as HTMLElement[]
if (documentDomList.length > 0) {
let listValue: string[] = []
documentDomList.forEach((fileDom: any) => {
listValueTd.push(fileDom.innerText)
listValue.push(fileDom.innerText)
})
value = listValue.join('\n')
}
let titleVal = dom1.innerText
labelTd.value[0].value = titleVal || ''
valueTd.value[0].value = value || ''
//处理每行的逻辑
if (valRow.tdList.length <= 2) {
// 单独占一行的情况1
if (node.classList.contains('lineRow') && valRow.tdList.length == 2) {
valRow.tdList[1].colspan = 20
resultList.push(JSON.parse(JSON.stringify(valRow)))
valRow.tdList = []
valRow.tdList.push(JSON.parse(JSON.stringify(labelTd)))
valRow.tdList.push(JSON.parse(JSON.stringify(valueTd)))
valRow.tdList[1].colspan = 20
resultList.push(JSON.parse(JSON.stringify(valRow)))
valRow.tdList = []
}
//单独占一行的情况2
else if (node.classList.contains("lineRow") && valRow.tdList.length == 0) {
valRow.tdList.push(JSON.parse(JSON.stringify(labelTd)))
valRow.tdList.push(JSON.parse(JSON.stringify(valueTd)))
valRow.tdList[1].colspan = 20
resultList.push(JSON.parse(JSON.stringify(valRow)))
valRow.tdList = []
} else {
valRow.tdList.push(JSON.parse(JSON.stringify(labelTd)))
valRow.tdList.push(JSON.parse(JSON.stringify(valueTd)))
}
} else {
resultList.push(JSON.parse(JSON.stringify(valRow)))
valRow.tdList = [];
valRow.tdList.push(JSON.parse(JSON.stringify(labelTd)))
valRow.tdList.push(JSON.parse(JSON.stringify(valueTd)))
}
}
}
})
})
}
//处理行行数据
if (valRow && valRow.tdList.length == 2) {
valRow.tdList[1].colspan = 20;
resultList.push(JSON.parse(JSON.stringify(valRow)))
valRow.tdList = [];
} else if (valRow && valRow.tdList.length == 4) {
resultList.push(JSON.parse(JSON.stringify(valRow)))
valRow.tdList = [];
}
titleRow.tdList[0].value[0].value = '审批意见'
resultList.push(JSON.parse(JSON.stringify(titleRow)))
return resultList
}
const printfFormFn = () => {
instance.command.executePrint()
}
defineExpose({
openDialog
})
</script>
页面调用方法
<template>
<div>
<el-button size="small" :icon="Printer" @click="PrintShowFn()">打印</el-button>
<div id="formDataDom">
<el-form ref="formRef" :model="form" label-width="150px" :inline="true" :rules="rules">
<div class="title">请假申请表</div>
<div class="pm-title">基本信息</div>
<!-- 这里写表单对应的相(有类lineRow的,占一行)-->
<el-form-item label="请假人" prop="stateName" class="borderRight">
<el-input v-model="form.userName" placeholder="请输入" />
</el-form-item>
<el-form-item label="请假原因" prop="stateName" class="borderRight">
<el-input v-model="form.userName" :rows="2" type="textarea" placeholder="请输入" />
</el-form-item>
</el-form>
</div>
<printf-form ref="printfFormRef"></printf-form>
</div>
</template>
<script lang="ts" setup name="printfForm">
const commentList=ref([])//审核意见数据
const form=ref<any>({})
const printfFormRef=ref()
const PrintShowFn = () => {
const domHtml = document.getElementById('formDataDom')
printfFormRef.value.openDialog(commentList.value, domHtml)
}
</script>
再次感谢canvas-editor
纯手打,转载标明出处,作者:iceBin