如何使用React JS或JS向预定义图片模板添加表格生成销售账单并下载
销售账单图片生成实现方案
核心实现逻辑
整体基于HTML Canvas实现绘制:先加载预定义的头部、尾部模板图片,再将表单提交的账单数据绘制成表格插入到中间位置,最终导出Canvas为图片文件实现下载。
原生JS实现方案
实现步骤
- 预加载模板资源:提前加载头部带logo的模板图、尾部模板图,确保资源加载完成后再触发绘制,避免出现空白区域。
- 搭建数据录入表单:配置商品名称、数量、单价、客户信息等需要的账单字段,提交按钮触发绘制流程。
- Canvas分层绘制:
- 初始化Canvas,根据头部高度、表格行数计算出的表格高度、尾部高度,设置Canvas整体宽高,宽度和模板宽度保持一致。
- 先绘制头部模板到Canvas顶部。
- 再绘制账单表格:按预设样式绘制表头,遍历账单数据逐行绘制内容,同时可按需绘制边框、分割线保持和模板风格统一。
- 最后绘制尾部模板到表格下方的对应位置。
- 导出下载:调用Canvas的
toDataURL方法转成图片base64,创建临时a标签触发下载即可。
核心代码示例
const TEMPLATE_WIDTH = 800; // 模板统一宽度 const HEADER_HEIGHT = 150; // 头部模板固定高度 const FOOTER_HEIGHT = 100; // 尾部模板固定高度 const ROW_HEIGHT = 40; // 表格单行高度 // 预加载模板资源 const headerImg = new Image(); headerImg.crossOrigin = 'anonymous'; headerImg.src = '你的头部模板资源路径'; const footerImg = new Image(); footerImg.crossOrigin = 'anonymous'; footerImg.src = '你的尾部模板资源路径'; // 提交触发绘制 document.getElementById('submitBtn').addEventListener('click', () => { const canvas = document.getElementById('billCanvas'); const ctx = canvas.getContext('2d'); // 获取表单录入的账单数据 const billItems = [ { name: document.getElementById('goodsName').value, num: Number(document.getElementById('goodsNum').value), price: Number(document.getElementById('goodsPrice').value) } // 可扩展多个商品条目 ]; // 计算整体画布高度 const tableHeight = (billItems.length + 1) * ROW_HEIGHT; // 加1为表头高度 canvas.width = TEMPLATE_WIDTH; canvas.height = HEADER_HEIGHT + tableHeight + FOOTER_HEIGHT; // 绘制头部 ctx.drawImage(headerImg, 0, 0, TEMPLATE_WIDTH, HEADER_HEIGHT); // 绘制表头 ctx.fillStyle = '#333'; ctx.font = '16px sans-serif'; ctx.fillText('商品名称', 50, HEADER_HEIGHT + 28); ctx.fillText('数量', 300, HEADER_HEIGHT + 28); ctx.fillText('单价', 450, HEADER_HEIGHT + 28); ctx.fillText('小计', 600, HEADER_HEIGHT + 28); // 绘制表格内容 billItems.forEach((item, index) => { const rowY = HEADER_HEIGHT + (index + 1) * ROW_HEIGHT + 28; ctx.fillText(item.name, 50, rowY); ctx.fillText(item.num, 300, rowY); ctx.fillText(item.price.toFixed(2), 450, rowY); ctx.fillText((item.num * item.price).toFixed(2), 600, rowY); // 可选绘制表格边框 ctx.strokeRect(0, HEADER_HEIGHT + (index + 1) * ROW_HEIGHT, TEMPLATE_WIDTH, ROW_HEIGHT); }); // 绘制尾部 ctx.drawImage(footerImg, 0, HEADER_HEIGHT + tableHeight, TEMPLATE_WIDTH, FOOTER_HEIGHT); // 触发下载 const link = document.createElement('a'); link.download = `销售账单_${Date.now()}.png`; link.href = canvas.toDataURL('image/png'); link.click(); });
React实现方案
可以直接复用上述原生Canvas绘制逻辑,也可以用react-konva等封装库简化DOM操作,核心流程和原生一致:
核心代码示例
import { useRef, useState, useEffect } from 'react'; const BillGenerator = () => { const canvasRef = useRef(null); const [billItems, setBillItems] = useState([{name: '', num: 0, price: 0}]); const [resourceLoaded, setResourceLoaded] = useState(false); const [headerImg, setHeaderImg] = useState(null); const [footerImg, setFooterImg] = useState(null); // 预加载模板资源 useEffect(() => { const loadImage = (src) => new Promise((resolve) => { const img = new Image(); img.crossOrigin = 'anonymous'; img.src = src; img.onload = () => resolve(img); }); Promise.all([loadImage('/header-template.png'), loadImage('/footer-template.png')]) .then(([hImg, fImg]) => { setHeaderImg(hImg); setFooterImg(fImg); setResourceLoaded(true); }); }, []); const handleGenerate = () => { if (!resourceLoaded) return; const canvas = canvasRef.current; const ctx = canvas.getContext('2d'); // 绘制逻辑和原生完全一致,此处省略重复代码 // 下载逻辑也和原生一致 }; return ( <div> {/* 账单录入表单 */} {billItems.map((item, idx) => ( <div key={idx} style={{marginBottom: '12px'}}> <input value={item.name} onChange={e => { const newItems = [...billItems]; newItems[idx].name = e.target.value; setBillItems(newItems); }} placeholder="商品名称" /> <input type="number" value={item.num} onChange={e => { const newItems = [...billItems]; newItems[idx].num = Number(e.target.value); setBillItems(newItems); }} placeholder="数量" /> <input type="number" value={item.price} onChange={e => { const newItems = [...billItems]; newItems[idx].price = Number(e.target.value); setBillItems(newItems); }} placeholder="单价" /> </div> ))} <button onClick={handleGenerate} disabled={!resourceLoaded}>生成并下载账单</button> <canvas ref={canvasRef} style={{display: 'none'}} /> </div> ); }; export default BillGenerator;
注意事项
- 跨域问题:如果模板图片存放在跨域域名下,需要资源服务端配置CORS响应头,同时给Image对象设置
crossOrigin = 'anonymous',否则导出时会触发Canvas安全污染报错。 - 清晰度优化:如果需要导出高清图片,可以将Canvas宽高设置为实际尺寸的2倍,所有绘制坐标对应放大2倍,最终导出的图片清晰度会明显提升。
- 样式对齐:提前测量模板的字体、颜色、边距参数,绘制表格时严格对齐参数,保证生成的图片和模板风格统一。
内容的提问来源于stack exchange,提问作者nihal mohammed
相关产品推荐
相关产品推荐

