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> </> ); }
关键细节说明
- 分页控制:通过CSS的
page-break-after属性确保每个页面独立分割,避免内容溢出 - 图片尺寸限制:设置商品图片最大宽高,防止图片撑破页面
- 总计位置:将发票总计放在最后一页,符合常规发票排版逻辑
- react-to-print适配:组件容器包含所有分页内容,确保打印时捕获完整页面
内容的提问来源于stack exchange,提问作者Mohan Dhila
相关产品推荐
相关产品推荐

