You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React发票打印适配A4:带图商品单独分页打印实现问询

解决React发票打印中带图商品分页问题

核心思路

  • 将发票拆分为独立页面单元,每个页面包含完整的页眉、商品内容、页脚
  • 带图商品单独占用一页,无图商品合并到同一页
  • 利用CSS分页属性强制页面分割,确保符合A4尺寸规范

具体解决方案

1. 添加打印专用CSS样式

在invoiceDesign.css中添加以下代码,控制页面尺寸和分页行为:

/* 打印样式 */
@media print {
  body {
    margin: 0;
    padding: 0;
  }
  .invoice-page {
    width: 210mm;
    height: 297mm;
    page-break-after: always;
    position: relative;
    overflow: hidden;
    padding: 15mm;
    box-sizing: border-box;
  }
  /* 最后一页取消自动分页 */
  .invoice-page:last-child {
    page-break-after: avoid;
  }
  /* 隐藏打印按钮 */
  .print-btn {
    display: none;
  }
}

/* 屏幕预览样式 */
.invoice-page {
  width: 210mm;
  min-height: 297mm;
  margin: 20px auto;
  border: 1px solid #ccc;
  padding: 15mm;
  box-sizing: border-box;
}

/* 限制商品图片尺寸 */
.invoice-item-image {
  max-width: 100%;
  height: auto;
  max-height: 150mm;
}

2. 重构React组件结构

第一步:创建单页发票子组件

在IvoiceDesign组件内定义InvoicePage,负责渲染单页的完整内容:

const InvoicePage = ({ 
  invoicenumber, 
  customerName, 
  customerGstNo, 
  formattedDate, 
  items, 
  totals,
  companyHeader,
  footerImage 
}) => {
  return (
    <div className="invoice-page">
      {/* 页眉 */}
      <div className='invoice-layout-header'>
        <p className='text-center m-0'>Invoice</p>
        <div className='invoice-header-image'>
          <img src={companyHeader} alt="Company Header" />
        </div>
        <div className='invoice-header-infomation'>
          <Row className='invoice-header-row m-1'>
            <Col sm={8} className='invoice-header-column'>
              <Row className='invoice-header-subrows m-1 flex'>
                <span className='p-0'><b>To:</b> {customerName}</span>
              </Row>
              <Row className='invoice-header-subrows m-1'>
                <span className='p-0'><b>GST No: </b>{customerGstNo || ""}</span>
              </Row>
            </Col>
            <Col sm={4} className='invoice-header-column'>
              <Row className='invoice-header-subrows m-1'>
                <span className='p-0'><b>Bill No :</b> {invoicenumber}</span>
              </Row>
              <Row className='invoice-header-subrows m-1'>
                <span className='p-0'><b>Date :</b> {formattedDate}</span>
              </Row>
            </Col>
          </Row>
        </div>
      </div>

      {/* 商品表格 */}
      <div className='invoice-table-container'>
        <table id="invoice-table">
          <colgroup>
            <col width="35mm" />
            <col />
            <col width="60mm" />
            <col width="100mm" />
            <col width="100mm" />
          </colgroup>
          <tr className="invoice_line">
            <th>sr.No</th>
            <th>Particurals</th>
            <th>Qty</th>
            <th>Rate</th>
            <th>Amount</th>
          </tr>
          {items.map((item, i) => (
            <tr className="invoice_line" key={item._id}>
              <td>{i + 1}</td>
              <td className="site">
                <span className='flex flex-column'>
                  <div className='text-black'>{item.title}</div>
                  {item.imageName && (
                    <img 
                      src={`http://localhost:3333/${item.imageName}`} 
                      alt="Item" 
                      className='invoice-item-image' 
                    />
                  )}
                  <div className='text-black'>{reactHtmlParser(item.description)}</div>
                </span>
              </td>
              <td className="amount"><b>{item.quantity}</b></td>
              <td className="amount"><b>{item.rate}</b></td>
              <td className="amount"><b>{item.itemAmount}</b></td>
            </tr>
          ))}
        </table>
      </div>

      {/* 总计信息(仅最后一页显示) */}
      {totals && (
        <div className="invoice-totals">
          <table id="invoice-totals">
            <tfoot>
              <tr className='mt-5'>
                <td colSpan={3}></td>
                <td><tr className='t-foot-td'>TOTAL</tr></td>
                <td><tr className='t-foot-td'><b>₹ {totals.amount}</b></td>
              </tr>
              {totals.IGST ? (
                <tr className='mt-5'>
                  <td colSpan={3}></td>
                  <td className='p-3'><tr className='t-foot-td p-4'>IGST({totals.GstRate[0].IGst}%)</tr></td>
                  <td><tr className='t-foot-td'><b>₹ {totals.IGST}</b></td>
                </tr>
              ) : (
                <>
                  <tr className='mt-5'>
                    <td colSpan={3}></td>
                    <td className='p-3'><tr className='t-foot-td p-4'>CGST({totals.GstRate[0].CGst}%)</tr></td>
                    <td><tr className='t-foot-td'><b>₹ {totals.CGST}</b></td>
                  </tr>
                  <tr className='mt-5'>
                    <td colSpan={3}></td>
                    <td className='p-3'><tr className='t-foot-td p-4'>SGST({totals.GstRate[0].SGst}%)</tr></td>
                    <td><tr className='t-foot-td'><b>₹ {totals.SGST}</b></td>
                  </tr>
                </>
              )}
              <tr className='mt-5'>
                <td colSpan={3}></td>
                <td><tr className='t-foot-td'>TOTAL</tr></td>
                <td><tr className='t-foot-td'><b>₹ {totals.invoiceTotalPrice}</b></td>
              </tr>
              <tr>
                <td colSpan="5" className='price-in-words'>
                  <span className='p-0 text-black'><b>In words: {converter.toWords(totals.invoiceTotalPrice)}</b></span>
                </td>
              </tr>
            </tfoot>
          </table>
        </div>
      )}

      {/* 页脚图片 */}
      <div className='invoice-footer-image' style={{ position: 'absolute', bottom: '10mm', width: '100%', left: 0, padding: '0 15mm' }}>
        <img src={footerImage} alt="Footer" style={{ width: '100%' }} />
      </div>
    </div>
  );
};

第二步:修改主组件逻辑,生成分页数据

在IvoiceDesign组件中添加状态和分组逻辑,拆分带图/无图商品:

function IvoiceDesign() {
    const { invoicenumber } = useParams()
    const componentRef = useRef();
    const [data, setData] = useState([])
    const [formattedDate, setFormattedDate] = useState('')
    const [isLoading, setIsLoading] = useState(true)
    const [pages, setPages] = useState([]) // 新增分页状态

    // ... 原有axios请求和日期格式化逻辑不变

    // 生成分页数据
    useEffect(() => {
      if (!isLoading && data.invoiceDetails) {
        const pageList = [];
        const nonImageItems = [];

        // 拆分带图和无图商品
        data.invoiceDetails.forEach(item => {
          if (item.imageName) {
            pageList.push({ items: [item] });
          } else {
            nonImageItems.push(item);
          }
        });

        // 无图商品合并为一页
        if (nonImageItems.length > 0) {
          pageList.push({ items: nonImageItems });
        }

        // 最后一页添加总计信息
        if (pageList.length > 0) {
          pageList[pageList.length - 1].totals = {
            amount: data.amount,
            invoiceTotalPrice: data.invoiceTotalPrice,
            IGST: data.IGST,
            CGST: data.CGST,
            SGST: data.SGST,
            GstRate: data.GstRate
          };
        }

        setPages(pageList);
      }
    }, [isLoading, data]);

    const handlePrint = useReactToPrint({
        content: () => componentRef.current
    });

    // 修改渲染部分
    return (
        <>
            {isLoading ? (
              <ReactLoading type='spin' color='blue' height='3rem' width='3rem' className='text-center m-auto' />
            ) : (
                <div className='container' >
                    <div ref={componentRef} className='sub-container'>
                      {/* 渲染所有分页 */}
                      {pages.map((page, index) => (
                        <InvoicePage
                          key={index}
                          invoicenumber={invoicenumber}
                          customerName={data.customerName}
                          customerGstNo={data.customerGstNo}
                          formattedDate={formattedDate}
                          items={page.items}
                          totals={page.totals}
                          companyHeader={image}
                          footerImage={footerImage}
                        />
                      ))}
                    </div>
                </div >
            )}
            <button className="print-btn" onClick={handlePrint}>
              <MdFileDownload size={30} />
            </button>
        </>
    );
}

关键细节说明

  1. 分页控制:通过CSS的page-break-after属性确保每个页面独立分割,避免内容溢出
  2. 图片尺寸限制:设置商品图片最大宽高,防止图片撑破页面
  3. 总计位置:将发票总计放在最后一页,符合常规发票排版逻辑
  4. react-to-print适配:组件容器包含所有分页内容,确保打印时捕获完整页面

内容的提问来源于stack exchange,提问作者Mohan Dhila

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 03:29:49