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

如何使用React JS或JS向预定义图片模板添加表格生成销售账单并下载

销售账单图片生成实现方案

核心实现逻辑

整体基于HTML Canvas实现绘制:先加载预定义的头部、尾部模板图片,再将表单提交的账单数据绘制成表格插入到中间位置,最终导出Canvas为图片文件实现下载。

原生JS实现方案

实现步骤

  • 预加载模板资源:提前加载头部带logo的模板图、尾部模板图,确保资源加载完成后再触发绘制,避免出现空白区域。
  • 搭建数据录入表单:配置商品名称、数量、单价、客户信息等需要的账单字段,提交按钮触发绘制流程。
  • Canvas分层绘制:
    1. 初始化Canvas,根据头部高度、表格行数计算出的表格高度、尾部高度,设置Canvas整体宽高,宽度和模板宽度保持一致。
    2. 先绘制头部模板到Canvas顶部。
    3. 再绘制账单表格:按预设样式绘制表头,遍历账单数据逐行绘制内容,同时可按需绘制边框、分割线保持和模板风格统一。
    4. 最后绘制尾部模板到表格下方的对应位置。
  • 导出下载:调用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.07 12:27:05