概述
本文介绍了一个基于 Vue 3 + Ant Design Vue 的表格打印分页技术实现方案。该方案主要用于客商评价明细表的打印功能,解决了复杂表格在打印时的分页控制问题。
图片概览

技术栈
- 前端框架: Vue 3 (Composition API)
- UI 组件库: Ant Design Vue
- 打印插件: v-print
- 样式预处理: SCSS
- 打印样式: CSS @page 规则
核心功能特性
1. 动态表格渲染
<tbody v-for="item in baseList" class="paging" style="height: 1px;">
<tr>
<td style="width: 130px;text-align: center;">{{ item.factor }}</td>
<td style="width: 100px;text-align: center;">否决项</td>
<td>{{ item.scoringcriteria }}</td>
<td style="width: 100px;text-align: center;">
<!-- 是/否选择框 -->
</td>
<td v-for="item in item.peo" class="tec">{{ item }}</td>
</tr>
</tbody>
2. 复杂表格结构处理
- 支持合并单元格 (
rowspan) - 动态列数渲染
- 嵌套循环表格数据
3. 分页算法
核心分页逻辑
const PAGE_HEIGHT = 595 // A4纸横向高度(landscape模式)
const printing = () => {
const splitDoms = document.getElementsByClassName('paging')
let startY = 0
let hasPageBreak = false
// 重置所有分页样式
for (let i = 0; i < splitDoms.length; i++) {
splitDoms[i].style.pageBreakBefore = ''
splitDoms[i].style.marginTop = ''
splitDoms[i].classList.remove('page-break-after')
}
for (let i = 0; i < splitDoms.length; i++) {
const splitDom = splitDoms[i]
const splitValue = splitDom.getBoundingClientRect()
if (startY === 0) {
startY = splitValue.top
}
const pageHeight = splitValue.bottom - startY
// 当加上当前div的高度大于A4纸高度时,给前一个div加上分页标识
if (pageHeight > PAGE_HEIGHT) {
startY = 0
hasPageBreak = true
if (i > 0) {
splitDoms[i - 1].style.pageBreakBefore = 'always'
splitDoms[i].style.marginTop = '20px'
splitDoms[i].classList.add('page-break-after')
}
}
}
}
分页算法特点
- 动态计算: 基于 DOM 元素的实际位置计算分页点
- A4 横向适配: 针对 A4 纸张横向打印优化
- 智能断页: 避免表格行被截断
- 样式重置: 每次打印前重置分页样式
4. 打印样式优化
CSS 打印样式
@page {
size: landscape; // 横向打印
margin: 0;
padding: 20px 0;
}
/* 防止表格跨页断开 */
table {
page-break-inside: avoid;
}
/* 防止段落跨页断开 */
p,
h1,
h2,
h3,
h4,
h5 {
page-break-after: avoid;
}
表格样式设计
.techniques {
width: 100%;
border-color: #000;
font-family: '宋体', 'SimSun', sans-serif;
td,
th {
border-collapse: collapse;
line-height: 22px;
font-size: 13px;
}
}
5. v-print 插件集成
打印配置
const printObj = {
id: 'mypdf',
popTitle: ' ',
extraCss: '',
extraHead: '',
preview: false,
previewTitle: 'xxxxxxxxxx',
previewPrintBtnLabel: '预览结束,开始打印',
zIndex: 10002,
// 各种回调函数
previewBeforeOpenCallback() {
console.log('正在加载预览窗口!')
},
previewOpenCallback() {
console.log('已经加载完预览窗口,预览打开了!')
},
beforeOpenCallback() {
console.log('开始打印之前!')
},
openCallback() {
console.log('执行打印了!')
},
closeCallback() {
console.log('关闭了打印工具!')
},
clickMounted() {
console.log('点击v-print绑定的按钮了')
}
}
数据结构设计
示例数据 1
let baseList = [
{
factor: '公司及董监高失信',
mainindex: '公司及董监高是否被列为失信执行人员',
score: '否决项',
scoringcriteria: '公司及董监高被列为失信执行人员',
standardScore: '',
peo: 5
},
{
factor: '成立年限',
mainindex: '成立年限',
score: '否决项',
scoringcriteria: '公司成立年限小于3年',
standardScore: '',
peo: 5
},
{
factor: '信用评价',
mainindex: '第三方征信系统的评级',
score: '否决项',
scoringcriteria:
'偿债能力预警(对应征信报告的第4级),如无第三方征信报告,则可从客商资产质量、资产负债率、营运管理、竞争力情况等方面进行综合评价,如资产质量较差,偿债能力较差,或存在大量诉讼案件或最近2年有重大行政处罚记录,需警惕',
standardScore: '',
peo: 5
},
{
factor: '净资产',
mainindex: '最新年度或半年度净资产',
score: '否决项',
scoringcriteria: '最新年度或半年度净资产总额小于500万元',
standardScore: '',
peo: 5
},
{
factor: '业务范围',
mainindex: '业务范围',
score: '否决项',
scoringcriteria: '有与公司同类自营贸易业务的民营企业',
standardScore: '',
peo: 5
},
{
factor: '经营性现金净流量',
mainindex: '经营性现金净流量',
score: '否决项',
scoringcriteria: '连续三年经营性现金净流量为负',
standardScore: '',
peo: 5
},
{
factor: '所有者权益与实收资本关系',
mainindex: '所有者权益与实收资本关系',
score: '否决项',
scoringcriteria: '所有者权益总额连续三年小于实收资本金额',
standardScore: '',
peo: 5
},
{
factor: '境内供应商未提供最近三年审计报告',
mainindex: '境内供应商未提供最近三年审计报告',
score: '否决项',
scoringcriteria: '境内供应商未提供最近三年审计报告',
standardScore: '',
peo: 5
}
]
示例数据 2
let orditemLsit = [
{
factor: '成立年限',
score: '5',
scoringcriteria: [
{
title: '成立年限5年(含)以上',
score: '5'
},
{
title: '成立年限3年(含)至5年',
score: '3'
},
{
title: '成立年限3年(不含)以下',
score: '1'
}
],
standardScore: '',
peo: 5
},
{
factor: '实缴注册资本金',
score: '5',
scoringcriteria: [
{
title: '8000万元(含) 以上 ',
score: '5'
},
{
title: '4000万元(含) 至8000万元 ',
score: '4'
},
{
title: '1000万元(含) 至4000万元 ',
score: '3'
},
{
title: '1000万元(不含)以下至4000万元 ',
score: '1'
}
],
standardScore: '',
peo: 5
},
{
factor: '企业性质',
score: '5',
scoringcriteria: [
{
title: '国有A股上市,央企三级以上公司 以上 ',
score: '5'
},
{
title: '上市公司,省属大型国企',
score: '4'
},
{
title: '国有非上市',
score: '3'
},
{
title: '其它(非国企、非上市)',
score: '1'
}
],
standardScore: '',
peo: 5
},
{
factor: '企业性质',
score: '5',
scoringcriteria: [
{
title: '国有A股上市,央企三级以上公司 以上 ',
score: '5'
},
{
title: '上市公司,省属大型国企',
score: '4'
},
{
title: '国有非上市',
score: '3'
},
{
title: '其它(非国企、非上市)',
score: '1'
}
],
standardScore: '',
peo: 5
},
{
factor: '企业性质',
score: '5',
scoringcriteria: [
{
title: '国有A股上市,央企三级以上公司 以上 ',
score: '5'
},
{
title: '上市公司,省属大型国企',
score: '4'
},
{
title: '国有非上市',
score: '3'
},
{
title: '其它(非国企、非上市)',
score: '1'
}
],
standardScore: '',
peo: 5
}
]
响应式分页
- 基于实际 DOM 高度动态计算分页点
- 避免硬编码分页规则
- 适应不同数据量的表格
完整代码
<template>
<div>
<a-button type="primary" style="margin-right: 10px;" @click="printing" v-print="printObj">打印</a-button>
<!-- <a-button type="primary" style="margin-right: 10px;" @click="downPdf">下载PDF</a-button> -->
</div>
<div style="overflow-y: auto;padding: 20px;">
<div class="main" id="mypdf">
<div class="title fd">测试附件打印</div>
<div class="fd">
供应商名称: <span>供应商一号</span>
</div>
<table border="1" class="techniques" style="margin-top: 1px;width: 100%;">
<tr>
<td style="width: 130px;text-align: center;">拟开展业务</td>
<td colspan="3">xxxxxxxxxxxxxxxxxxxxxx</td>
<td class="tec" :colspan="5">评价明细情况</td>
</tr>
<tr>
<!-- <td style="width: 130px;text-align: center;">因素</td> -->
<td style="width: 130px;text-align: center;">主要指标</td>
<td style="width: 100px;text-align: center;">指标类型</td>
<td style="width: 150px;">评分标准</td>
<td style="width: 100px" class="tec">标准分数</td>
<td v-for="item in 5" class="tec">姓名{{ item }}</td>
</tr>
<tbody v-for="item in baseList" class="paging" style="height: 1px;">
<tr>
<!-- <td style="width: 130px;text-align: center;">{{ item.factor }}</td> -->
<td style="width: 130px;text-align: center;">{{ item.factor }}</td>
<td style="width: 100px;text-align: center;">否决项</td>
<td>{{ item.scoringcriteria }}</td>
<td style="width: 100px;text-align: center;">
<div style="display: flex;">
<div style="display: flex; align-items: center;">
<div class="tag">
<!-- <check-outlined class="tagIcon" /> -->
</div>
<div>是</div>
</div>
<div style="display: flex; align-items: center;">
<div class="tag">
<!-- <check-outlined class="tagIcon" /> -->
</div>
<div>否</div>
</div>
</div>
</td>
<td v-for="item in item.peo" class="tec">{{ item }}</td>
</tr>
</tbody>
<tbody v-for="item in orditemLsit" :key="item.id" class="paging">
<tr v-for="(val, index) in item.scoringcriteria" :key="val.id">
<td style="width: 130px;text-align: center;" v-if="index === 0"
:rowspan="item.scoringcriteria.length">
{{ item.factor }}
</td>
<td style="width: 100px;text-align: center;" v-if="index === 0"
:rowspan="item.scoringcriteria.length">
5
</td>
<td style="width: 130px;">{{ val.title }}</td>
<td style="width: 100px;" class="tec">{{ val.score }}</td>
<template v-for="items in 5">
<td class="tec" v-if="index === 0" :rowspan="item.scoringcriteria.length">{{ items }}</td>
</template>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup>
let baseList = [
{
factor: '公司及董监高失信',
mainindex: '公司及董监高是否被列为失信执行人员',
score: '否决项',
scoringcriteria: '公司及董监高被列为失信执行人员',
standardScore: '',
peo: 5
},
{
factor: '成立年限',
mainindex: '成立年限',
score: '否决项',
scoringcriteria: '公司成立年限小于3年',
standardScore: '',
peo: 5
},
{
factor: '信用评价',
mainindex: '第三方征信系统的评级',
score: '否决项',
scoringcriteria: '偿债能力预警(对应征信报告的第4级),如无第三方征信报告,则可从客商资产质量、资产负债率、营运管理、竞争力情况等方面进行综合评价,如资产质量较差,偿债能力较差,或存在大量诉讼案件或最近2年有重大行政处罚记录,需警惕',
standardScore: '',
peo: 5
},
{
factor: '净资产',
mainindex: '最新年度或半年度净资产',
score: '否决项',
scoringcriteria: '最新年度或半年度净资产总额小于500万元',
standardScore: '',
peo: 5
},
{
factor: '业务范围',
mainindex: '业务范围',
score: '否决项',
scoringcriteria: '有与公司同类自营贸易业务的民营企业',
standardScore: '',
peo: 5
},
{
factor: '经营性现金净流量',
mainindex: '经营性现金净流量',
score: '否决项',
scoringcriteria: '连续三年经营性现金净流量为负',
standardScore: '',
peo: 5
},
{
factor: '所有者权益与实收资本关系',
mainindex: '所有者权益与实收资本关系',
score: '否决项',
scoringcriteria: '所有者权益总额连续三年小于实收资本金额',
standardScore: '',
peo: 5
},
{
factor: '境内供应商未提供最近三年审计报告',
mainindex: '境内供应商未提供最近三年审计报告',
score: '否决项',
scoringcriteria: '境内供应商未提供最近三年审计报告',
standardScore: '',
peo: 5
},
]
let orditemLsit = [
{
factor: '成立年限',
score: '5',
scoringcriteria: [
{
title: '成立年限5年(含)以上',
score: '5'
},
{
title: '成立年限3年(含)至5年',
score: '3'
},
{
title: '成立年限3年(不含)以下',
score: '1'
},
],
standardScore: '',
peo: 5
},
{
factor: '实缴注册资本金',
score: '5',
scoringcriteria: [
{
title: '8000万元(含) 以上 ',
score: '5'
},
{
title: '4000万元(含) 至8000万元 ',
score: '4'
},
{
title: '1000万元(含) 至4000万元 ',
score: '3'
},
{
title: '1000万元(不含)以下至4000万元 ',
score: '1'
},
],
standardScore: '',
peo: 5
},
{
factor: '企业性质',
score: '5',
scoringcriteria: [
{
title: '国有A股上市,央企三级以上公司 以上 ',
score: '5'
},
{
title: '上市公司,省属大型国企',
score: '4'
},
{
title: '国有非上市',
score: '3'
},
{
title: '其它(非国企、非上市)',
score: '1'
},
],
standardScore: '',
peo: 5
},
{
factor: '企业性质',
score: '5',
scoringcriteria: [
{
title: '国有A股上市,央企三级以上公司 以上 ',
score: '5'
},
{
title: '上市公司,省属大型国企',
score: '4'
},
{
title: '国有非上市',
score: '3'
},
{
title: '其它(非国企、非上市)',
score: '1'
},
],
standardScore: '',
peo: 5
},
{
factor: '企业性质',
score: '5',
scoringcriteria: [
{
title: '国有A股上市,央企三级以上公司 以上 ',
score: '5'
},
{
title: '上市公司,省属大型国企',
score: '4'
},
{
title: '国有非上市',
score: '3'
},
{
title: '其它(非国企、非上市)',
score: '1'
},
],
standardScore: '',
peo: 5
},
]
const printObj = {
id: "mypdf", // 这里是要打印元素的ID
popTitle: " ", // 打印的标题
extraCss: "", // 打印可引入外部的一个 css 文件
extraHead: "", // 打印头部文字
preview: false, // 是否启动预览模式,默认是false
previewTitle: 'xxxxxxxxxx', // 打印预览的标题
previewPrintBtnLabel: '预览结束,开始打印', // 打印预览的标题下方的按钮文本,点击可进入打印
zIndex: 10002, // 预览窗口的z-index,默认是20002,最好比默认值更高
previewBeforeOpenCallback() {
console.log('正在加载预览窗口!')
},
previewOpenCallback() { console.log('已经加载完预览窗口,预览打开了!') }, // 预览窗口打开时的callback
beforeOpenCallback() {
console.log('开始打印之前!')
}, // 开始打印之前的callback
openCallback() {
console.log('执行打印了!')
}, // 调用打印时的callback
closeCallback() { console.log('关闭了打印工具!') }, // 关闭打印的callback(无法区分确认or取消)
clickMounted() {
console.log('点击v-print绑定的按钮了')
},
}
const PAGE_HEIGHT = 595 // A4纸横向高度(landscape模式)
const printing = () => {
const splitDoms = document.getElementsByClassName('paging')
console.log(splitDoms)
let startY = 0 // 占用A4纸的高度,从每页第一个poetry div的top值开始累加
let hasPageBreak = false // 标记是否有分页
// 重置所有分页样式
for (let i = 0; i < splitDoms.length; i++) {
splitDoms[i].style.pageBreakBefore = '';
splitDoms[i].style.marginTop = '';
splitDoms[i].classList.remove('page-break-after');
}
for (let i = 0; i < splitDoms.length; i++) {
const splitDom = splitDoms[i]
const splitValue = splitDom.getBoundingClientRect()
console.log(splitDom.getBoundingClientRect())
if (startY === 0) {
startY = splitValue.top
}
const pageHeight = splitValue.bottom - startY
// 当加上当前div的高度大于A4纸高度时,给前一个div加上分页标识
if (pageHeight > PAGE_HEIGHT) {
console.log(i)
startY = 0
hasPageBreak = true
if (i > 0) {
splitDoms[i - 1].style.pageBreakBefore = 'always'; // 给前一个元素添加分页符
// 为分页后的元素添加上边距
splitDoms[i].style.marginTop = '20px';
splitDoms[i].classList.add('page-break-after');
}
}
}
}
</script>
<style lang="scss" scoped>
.main {
width: 900px;
margin: 0 auto;
}
.title {
font-size: 19px;
margin-bottom: 10px;
line-height: 33px;
text-align: center;
}
.techniques {
width: 100%;
border-color: #000;
font-family: "宋体", "SimSun", sans-serif;
}
.techniques,
.techniques th,
.techniques td {
border-collapse: collapse;
line-height: 22px;
font-size: 13px
}
.fd {
font-weight: bold;
}
.tec {
text-align: center;
}
.tag {
width: 14px;
height: 14px;
border-radius: 2px;
text-align: center;
color: #fff;
border: 1px solid #333;
font-weight: 700;
margin: 0 5px;
position: relative;
.tagIcon {
position: absolute;
font-size: 12px;
top: 1px;
left: -20px;
z-index: 111;
color: #000;
}
}
@page {
size: landscape;
margin: 0;
padding: 20px 0;
}
@media print {}
/* 防止表格跨页断开 */
table {
page-break-inside: avoid;
}
/* 防止段落跨页断开 */
p,
h1,
h2,
h3,
h4,
h5 {
page-break-after: avoid;
}
</style>
2256

被折叠的 条评论
为什么被折叠?



