前端打印(三联纸票据打印)

需要满足批量打印,每个单据由三部分组成基础信息+表格数据+落款。当表格数据过多出现

分页时,需要实现每页都包含基础信息和落款,只需要拆分表格数据。

一、核心思路

1、结构拆分:将单条单据拆分为基础信息、表格容器、页脚(落款)三个独立模块;

2、打印样式:

1)页眉/页脚用position: fixed固定每一个的顶部/底部;

2)表格容器预留也没/页脚空间,避免内容重叠;

3)表格行用page-break-inside: avoid 防止行被截断,表格超出时自动分页;

3、适配三联纸:保留纸张尺寸,优化边距和字体适配针式打印

二、代码实现

javascript 复制代码
<template>
  <div class="fa-wrap">
    <div class="or-box" v-for="(pageItem, pageIdx) in finalOrderPages" :key="pageItem.uniqueKey">
      <!-- 基础数据(页眉)- 每页都显示 -->
      <div class="print-header">
        <div class="m-title">{{ pageItem.companyName }}</div>
        <div class="pur-title">项目发货单</div>
        <div class="in-row">
          <div class="in-item in-item-first">
            <span class="in-lable">项目名称:</span>
            <span class="in-val">{{ pageItem.projectName }}</span>
          </div>
          <div class="in-item">
            <span class="in-lable">发货日期:</span>
            <span class="in-val">{{ pageItem.documentDate }}</span>
          </div>
          <div class="in-item">
            <span class="in-lable">页码:</span>
            <span class="in-val">{{ `${pageItem.pageNum}/${pageItem.totalPages}` }}</span>
          </div>
        </div>
        <div class="in-row">
          <div class="in-item in-item-first">
            <span class="in-lable">项目编号:</span>
            <span class="in-val">{{ pageItem.projectCode }}</span>
          </div>
          <div class="in-item">
            <span class="in-lable">销售单号:</span>
            <span class="in-val">{{ pageItem.sourceCode }}</span>
          </div>
          <div class="in-item">
            <span class="in-lable">编号:</span>
            <span class="in-val">{{ pageItem.documentCode }}</span>
          </div>
        </div>
        <div class="in-row">
          <div class="in-item">备注:{{ pageItem.remark }}</div>
        </div>
      </div>
      <!-- 表格部分 - 每页显示对应拆分的数据 -->
      <div class="print-table-container">
        <table class="table-block">
          <thead>
            <tr>
              <th width="5%">序号</th>
              <th width="12%">产品代码</th>
              <th width="20%">产品名称</th>
              <th width="15%">规格型号</th>
              <th width="12%">发货仓库</th>
              <th width="8%">单位</th>
              <th width="8%">应发数量</th>
              <th width="8%">实发数量</th>
              <th width="12%">备注</th>
            </tr>
          </thead>
          <tbody>
            <!-- 当前页的表格数据 -->
            <tr v-for="(sub, idx) in pageItem.pagePartList" :key="sub.id + '_' + pageIdx">
              <td>{{ pageItem.startSerial + idx }}</td> <!-- 累计序号 -->
              <td>{{ sub.partCode }}</td>
              <td class="text-l">{{ sub.partName }}</td>
              <td>{{ sub.partSpec || " " }}</td>
              <td>{{ sub.warehouse || " " }}</td>
              <td>{{ sub.unit || " " }}</td>
              <td>{{ sub.pendingNum }}</td>
              <td>{{ " " }}</td>
              <td>{{ sub.remark || " " }}</td>
            </tr>
            <!-- 合计行:仅最后一页显示 -->
            <tr v-if="pageItem.isLastPage">
              <td colspan="6" style="text-align: left">合计:</td>
              <td>{{ fieldSummary(pageItem.originalPartList, "pendingNum") }}</td>
              <td></td>
              <td></td>
            </tr>
          </tbody>
        </table>
      </div>
      <!-- 落款(页脚)- 每页都显示 -->
      <div class="print-footer">
        <div class="in-row">
          <div class="in-item in-item-first">
            <span class="in-lable">送货地址:</span>
            <div class="in-val">{{ pageItem.address }}</div>
          </div>
          <div class="in-item">
            <span class="in-lable">联系人:</span>
            <div class="in-val">{{ pageItem.contactPerson }}</div>
          </div>
          <div class="in-item">
            <span class="in-lable">联系电话:</span>
            <div class="in-val">{{ pageItem.contactPhone }}</div>
          </div>
        </div>
        <div class="in-row in-row-spe">
          <div class="in-item">
            <span class="in-lable">制单人:</span>
            <div class="in-val">{{ pageItem.createBy }}</div>
          </div>
          <div class="in-item">
            <span class="in-lable">审核人:</span>
            <div class="in-val">{{ pageItem.auditName }}</div>
          </div>
          <div class="in-item">
            <span class="in-lable">发料人:</span>
            <div class="in-val"></div>
          </div>
          <div class="in-item">
            <span class="in-lable">签收人:</span>
            <div class="in-val"></div>
          </div>
        </div>
        <div class="last-row">
          <span>白联:存根</span>
          <span>红联:财务</span>
          <span>黄联:接收单位</span>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { formatNumber } from "@/lib/tools.js";
import { fieldSummary } from "./templatePrint.js";

export default {
  name: "BillPrint",
  props: {
    printData: {
      type: Object,
      default: () => ({}),
    },
  },
  data() {
    return {
      finalOrderPages: [], // 最终拆分后的所有单据页(每页都是完整单据)
      pageSize: 6, // 每页表格最大行数(根据三联纸高度调整)
    };
  },
  created() {
    this.initAndSplitData();
  },
  methods: {
    formatNumber,
    fieldSummary,
    initAndSplitData() {
      const originalList = this.printData.data || [];
      this.finalOrderPages = [];
      // 遍历每一个原始单据
      originalList.forEach((originalItem) => {
        const partList = originalItem.partList || [];
        const totalRows = partList.length;
        const totalPages = Math.ceil(totalRows / this.pageSize); // 计算总页数
        // 为每一页生成独立的完整单据
        for (let pageNum = 1; pageNum <= totalPages; pageNum++) {
          // 计算当前页的表格数据范围
          const startIdx = (pageNum - 1) * this.pageSize;
          const endIdx = Math.min(pageNum * this.pageSize, totalRows);
          const pagePartList = partList.slice(startIdx, endIdx); // 当前页表格数据

          // 构造当前页的完整单据(复制基础数据+当前页表格+标记分页信息)
          const pageItem = {
            ...originalItem, // 复制所有基础数据(公司名、项目名、地址等)
            uniqueKey: `${originalItem.id || Date.now()}_page_${pageNum}`, // 唯一Key
            pageNum: pageNum, // 当前页码
            totalPages: totalPages, // 总页数
            pagePartList: pagePartList, // 当前页表格数据
            originalPartList: partList, // 原始完整表格数据(用于合计)
            isLastPage: pageNum === totalPages, // 是否最后一页(控制合计行显示)
            startSerial: startIdx + 1, // 当前页起始序号(保证累计)
          };

          // 加入最终分页列表
          this.finalOrderPages.push(pageItem);
        }
      });
    },
  },
};
</script>

<style lang="scss" scoped>
.fa-wrap {
  width: 100%;
  font-family: "宋体", SimSun, sans-serif;
  box-sizing: border-box;
  padding: 5px 0px;
  .or-box {
    margin-bottom: 30px;
  }
  .print-header {
    margin-bottom: 15px;
  }
  .print-table-container {
    margin: 15px 0;
  }
  .print-footer {
    margin-top: 15px;
  }
  .m-title {
    width: 100% !important;
    font-size: 30px;
    font-weight: bold;
    line-height: 40px;
    text-align: center !important;
  }
  .pur-title {
    width: 100% !important;
    font-size: 26px;
    margin: 15px auto;
    text-align: center;
    font-weight: bold;
  }

  .in-row {
    display: flex;
    flex-wrap: nowrap;
    margin-bottom: 8px;

    .in-item {
      display: flex;
      align-items: flex-start;
      margin-right: 40px;
      margin-bottom: 10px;
      min-width: 250px;
      font-size: 22px;
      font-weight: bold;

      .in-lable {
        white-space: nowrap;
        margin-right: 5px;
      }

      .in-val {
        word-break: break-all;
        flex: 1;
        white-space: nowrap;
      }
    }

    .in-item-first {
      min-width: 400px;
      .in-val {
        white-space: pre-wrap;
      }
    }
  }

  .in-row-spe {
    width: 100% !important;
    border-bottom: 1px solid #000;
    padding-bottom: 5px;
    margin-bottom: 10px;

    .in-item {
      margin-bottom: 5px;
    }
  }

  .last-row {
    width: 100% !important;
    height: 40px;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 20px;
    font-weight: bold;

    span {
      margin: 0 10px;
    }
  }
}

// 表格样式
.table-block {
  width: 100%;
  border-collapse: collapse;
  border: 1px solid #000 !important;

  td,
  th {
    border: 1px solid #000 !important;
    padding: 8px 5px !important;
    vertical-align: middle;
    box-sizing: border-box;
    text-align: center;
    font-size: 20px !important;
    font-weight: bold !important;
  }

  th {
    padding: 12px 5px !important;
    background-color: #f5f5f5;
  }

  .text-l {
    text-align: left !important;
    padding-left: 8px !important;
  }

  td:not([colspan]):empty {
    border: none !important;
  }
}
// 打印专属样式(核心:强制每页独立打印)
@media print {
  // 三联纸尺寸配置
  @page {
    size: 241mm 140mm; // 三联纸标准尺寸
    margin: 5mm 3mm; // 窄边距适配针式打印
    // 隐藏浏览器默认页眉页脚
    @top-center { content: ""; }
    @bottom-center { content: ""; }
  }
  // 每个单据页强制分页,且内部不拆分
  .or-box {
    page-break-after: always; // 每页单据后强制分页
    page-break-inside: avoid; // 避免单据内容被拆分到两页
    margin: 0;
    padding: 0;
    border: none;
  }
  // 最后一页取消强制分页(避免空白页)
  .or-box:last-child {
    page-break-after: avoid;
  }
  // 打印时的页眉/表格/页脚样式
  .print-header, .print-footer {
    background: #fff !important;
    width: 100%;
  }
  // 表格表头每页重复显示
  .table-block thead {
    display: table-header-group !important;
  }
  // 隐藏非打印元素
  button, .no-print {
    display: none !important;
  }
}
</style>

三、效果图

这世界很喧嚣,做你自己就好

相关推荐
ywf12151 小时前
前端的dist包放到后端springboot项目下一起打包
前端·spring boot·后端
恋猫de小郭1 小时前
2026,Android Compose 终于支持 Hot Reload 了,但是收费
android·前端·flutter
hpoenixf7 小时前
2026 年前端面试问什么
前端·面试
还是大剑师兰特7 小时前
Vue3 中的 defineExpose 完全指南
前端·javascript·vue.js
泯泷7 小时前
阶段一:从 0 看懂 JSVMP 架构,先在脑子里搭出一台最小 JSVM
前端·javascript·架构
mengchanmian8 小时前
前端node常用配置
前端
华洛8 小时前
利好打工人,openclaw不是企业提效工具,而是个人助理
前端·javascript·产品经理
xkxnq8 小时前
第六阶段:Vue生态高级整合与优化(第93天)Element Plus进阶:自定义主题(变量覆盖)+ 全局配置与组件按需加载优化
前端·javascript·vue.js
A黄俊辉A9 小时前
vue css中 :global的使用
前端·javascript·vue.js
小码哥_常10 小时前
被EdgeToEdge适配折磨疯了,谁懂!
前端