IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> node+koa+canvas绘制出货单,收据,票据 -> 正文阅读

[JavaScript知识库]node+koa+canvas绘制出货单,收据,票据

在生成票据需求中,我们会想到前端生成或者后端生成返回图片地址访问两个方法,前端生成则不需要调用接口,而后端是在完成整个流程时就进行生成然后把上传的地址保存数据库

先看效果

?

下面我们就使用node +koa+canvas后端生成图片的方法进行生成

使用库

1、node
2、canvas npm install canvas
3、koa npm install koa
4、mime-types npm install mime-types -S

首先创建服务 index.js

把用到的库都导入进去,当然如何创建node项目我这就不做过多的描述,创建成功后,直接使用 node index.js 就可以启动服务了

const Koa = require('koa')
const app = new Koa()
const {createCanvas, Image} = require('canvas');
const router = require('koa-router')(); /*引入是实例化路由 推荐*/

//....这里需要做很多事

app.use(router.routes())
app.use(router.allowedMethods())
app.listen(3000)

创建一个api 提供外面可访问的接口api

在末尾加了一个供外面访问的接口,启动服务后 访问localhost:3000/img 就可以访问了

const Koa = require('koa')
const app = new Koa()
const {createCanvas, Image} = require('canvas');
const router = require('koa-router')(); /*引入是实例化路由 推荐*/

//....这里需要做很多事

router.get('/img', async (ctx) => { });

app.use(router.routes())
app.use(router.allowedMethods())
app.listen(3000)

ok 服务已经好了,正片开始,瓜子饮料矿泉水,前面的麻烦让一让,

首先我没得知道,票据单有哪些内容

1、标题:编号,日期,地址;这些都是文字,所以我没得绘制文字
2、表格:表头,内容,线条;表格就是线条堆积而成的,内容就是文字,这里就得绘制线条,文字
3、尾部:文字,印章,签名;这里需要绘制文字,图片两个
总的来说,我们想要绘制出一张票据单,得要绘制文字,绘制线条,通过线条与文字结合生成表格,最后添加印章与签名照片

绘制画布

1、给画布设置长宽

  const width = 700
  const height = 460
  const canvas = createCanvas(width, height)
  const context = canvas.getContext('2d')


2、创建画布 给画布添加背景颜色

  const createMyCanvas=()=>{
      context.fillStyle = '#a2bcd3'
      context.fillRect(0, 0, width, height)
  }

画布添加文字函数

  /**
    * @writeTxt: canvas 写入字内容
    * @param {str} t 内容
    * @param {str} s 字体大小 粗体
    * @param {arr} p 写入文字的位置
    * @param {arr} a 写入文字对齐方式 默认 居中
    * @param {obj} c 写入文字颜色 默认 #000
    */
  const writeTxt = (t, s='normal bold 12px', p, a = 'center', c = '#000') => {
      if (!t) {
        return;
      }
      context.font = `${s} 黑体`;
      context.fillStyle = c;
      context.textAlign = a;
      context.textDecoration='underline'
      context.textBaseline = 'middle';
      context.fillText(t, p[0], p[1]);
  }

画布绘表格线条函数

/**
    * @drawLine: 画table线
    * @param list {arr} 表格竖线x轴位置
    * @param tlist {arr} 表格需要填写文字 无文字 填 ''
    * @param startHei {num} 开始画线的高度
    * @param lineWidth {num} 横线的长度
    * @param n {num} 行数
    * @param txtHei {num} 文字位置调整
    * @param isTrue {boolean} 是否为物资列表
    */
  const drawLine = (list, tlist, startHei, lineWidth, n, txtHei = 14, isTrue = false) => {
      for (let i = 0; i < n; i++) {
          for (let y in list) {
              if (+y !== 0) {
                  const poi = list[y] - (list[y] - list[y - 1]) / 2;
                  writeTxt(tlist[i][y - 1], '12px', [poi, startHei + txtHei + 30 * i])
              }
              context.moveTo(list[y], startHei);
              context.lineTo(list[y], startHei + 30 * (i + 1));
          }
          if (isTrue) {
                   const mtY = startHei + 30 * n;
                   if (i == 0) {
                       context.moveTo(10, startHei + 30 * i);
                       context.lineTo(690, startHei + 30 * i);
                   }
                   context.moveTo(10, mtY);
                   context.lineTo(690, mtY);
          }
          context.moveTo(10, startHei + 30 * i);
          context.lineTo(lineWidth, startHei + 30 * i);
      }
      if (isTrue) {
          const mtY = startHei + 30 * n;
          context.moveTo(10, mtY);
          context.lineTo(690, mtY);
      }
      context.strokeStyle = '#5a5a59';
      context.stroke();
  }

绘制表格

/**
* @drawTable: 画表格
  */
  const drawTable = () => {

      const titleArr = [10, 100, 290, 360, 430, 500, 600, 690];
      const titleTxtArr = [
        ['货号', '名称及规格', '单位', '数量', '单价', '金额', '备注']
      ]
      const goodsTxtArr = [
          ['', '', '', '', '', '', ''],
          ['', '', '', '', '', '', ''],
          ['', '', '', '', '', '', ''],
          ['', '', '', '', '', '', ''],
          ['', '', '', '', '', '', '']
      ]
      const bottomArr=[10,100,690]
      const bottomTxtArr=[
        ['合计大写', ' 拾 万 仟 佰 拾 元 角 分 ¥ ']
      ]
      drawLine(titleArr, titleTxtArr, 120, 690, 1, 16)
      drawLine(titleArr, goodsTxtArr, 151, 690, goodsTxtArr.length, 16, true)
      drawLine(bottomArr, bottomTxtArr, 301, 690, bottomTxtArr.length, 16,true)
  }

绘制图片,这里绘制图片其实就是绘制印章

/**
* 添加图片
* @param imgPath 图片路径 和图片所在位置
* @returns {Promise<void>}
  */
  const drawImg = async (imgPath = [{imgUrl: '', position: []}]) => {
    let len = imgPath.length
    for (let i = 0; i < len; i++) {
      const image = await loadImage(imgPath[i].imgUrl)
      context.drawImage(image, ...imgPath[i].position)
    }

  }

ok,相关绘制的函数已经好了,那么接下来就是进行排版了

1、首先的是头部的标题,单位,位置 编号,和时间

//创建画布
createMyCanvas()
//开始绘制
context.beginPath()
writeTxt('送 货 单', 'normal bold 30px', [370, 30])
writeTxt('No', '20px', [450, 34])
writeTxt('收货单地址:XXXXX', '14px', [12, 70], 'left')
writeTxt('地     址:XXXXXXXXXXX', '14px', [12, 100], 'left')
writeTxt('2022 年 9 月 22 日', '14px', [680, 100], 'right')

2、表格部分,绘制表头,表格,表尾

这里直接调用绘制表格的函数就可以了

3、票据尾部,签章,签字

writeTxt('收货单位及经手人(签章):', '14px', [12, 350], 'left')
writeTxt('送货单位及经手人(签章):', '14px', [400, 350],)
const imgList = [
    {
        // imgUrl: 'https://profile.csdnimg.cn/4/1/C/0_weixin_41277748',
        imgUrl: path.join(__dirname + '/reject.png'),
        position: [180, 350, 90, 80]
    },
    {
        imgUrl: path.join(__dirname + '/pass.png'),
        // imgUrl: 'https://profile.csdnimg.cn/4/1/C/0_weixin_41277748',
        position: [500, 350, 90, 80]
    },
]
//绘制签章
await drawImg(imgList)
//签名
writeTxt('井底的蜗牛', '24px', [240, 370],)
writeTxt('井底的蜗牛', '24px', [550, 370],)

到这里,完整的票据就好了

完整代码

const Koa = require('koa')
const app = new Koa()
const {createCanvas, loadImage, Image} = require('canvas');
const qr = require('qr-image');
const router = require('koa-router')(); /*引入是实例化路由 推荐*/

const path = require("path")
const fs = require("fs")

const width = 700
const height = 460
const canvas = createCanvas(width, height)
const context = canvas.getContext('2d')

/**
 * @writeTxt: canvas 写入字内容
 * @param {str} t 内容
 * @param {str} s 字体大小 粗体
 * @param {arr} p 写入文字的位置
 * @param {arr} a 写入文字对齐方式 默认 居中
 * @param {obj} c 写入文字颜色 默认 #000
 */
const writeTxt = (t, s = 'normal bold 12px', p, a = 'center', c = '#000') => {
    if (!t) {
        return;
    }
    context.font = `${s} 黑体`;
    context.fillStyle = c;
    context.textAlign = a;
    context.textDecoration = 'underline'
    context.textBaseline = 'middle';
    context.fillText(t, p[0], p[1]);
}
/**
 * @drawTable: 画表格
 */
const drawTable = () => {

    const titleArr = [10, 100, 290, 360, 430, 500, 600, 690];
    const titleTxtArr = [
        ['货号', '名称及规格', '单位', '数量', '单价', '金额', '备注']
    ]
    const goodsTxtArr = [
        ['', '', '', '', '', '', ''],
        ['', '', '', '', '', '', ''],
        ['', '', '', '', '', '', ''],
        ['', '', '', '', '', '', ''],
        ['', '', '', '', '', '', '']
    ]
    const bottomArr = [10, 100, 690]
    const bottomTxtArr = [
        ['合计大写', ' 拾 万 仟 佰 拾 元 角 分 ¥ ']
    ]
    drawLine(titleArr, titleTxtArr, 120, 690, 1, 16)
    drawLine(titleArr, goodsTxtArr, 151, 690, goodsTxtArr.length, 16, true)
    drawLine(bottomArr, bottomTxtArr, 301, 690, bottomTxtArr.length, 16, true)
}


/**
 * @drawLine: 画table线
 * @param list {arr} 表格竖线x轴位置
 * @param tlist {arr} 表格需要填写文字 无文字 填 ''
 * @param startHei {num} 开始画线的高度
 * @param lineWidth {num} 横线的长度
 * @param n {num} 行数
 * @param txtHei {num} 文字位置调整
 * @param isTrue {boolean} 是否为物资列表
 */
const drawLine = (list, tlist, startHei, lineWidth, n, txtHei = 14, isTrue = false) => {
    for (let i = 0; i < n; i++) {
        for (let y in list) {
            if (+y !== 0) {
                const poi = list[y] - (list[y] - list[y - 1]) / 2;
                writeTxt(tlist[i][y - 1], '12px', [poi, startHei + txtHei + 30 * i])
            }

            context.moveTo(list[y], startHei);
            context.lineTo(list[y], startHei + 30 * (i + 1));
        }

        if (isTrue) {
            const mtY = startHei + 30 * n;

            if (i == 0) {
                context.moveTo(10, startHei + 30 * i);
                context.lineTo(690, startHei + 30 * i);
            }

            context.moveTo(10, mtY);
            context.lineTo(690, mtY);
        }
        context.moveTo(10, startHei + 30 * i);
        context.lineTo(lineWidth, startHei + 30 * i);
    }

    if (isTrue) {
        const mtY = startHei + 30 * n;
        context.moveTo(10, mtY);
        context.lineTo(690, mtY);
    }

    context.strokeStyle = '#5a5a59';
    context.stroke();
}



/**
 * 添加图片
 * @param imgPath 图片路径 和图片所在位置,图片路径是绝对路径,可以使用path的方法去读取
 * @returns {Promise<void>}
 */
const drawImg = async (imgPath = [{imgUrl: '', position: []}]) => {
    let len = imgPath.length
    for (let i = 0; i < len; i++) {
        const image = await loadImage(imgPath[i].imgUrl)
        context.drawImage(image, ...imgPath[i].position)
    }

}


// 创建画布
const createMyCanvas = () => {
    context.fillStyle = '#a2bcd3'
    context.fillRect(0, 0, width, height)
}
const mime = require('mime-types');

router.get('/img', async (ctx) => {
    //创建画布
    createMyCanvas()
    //开始绘制
    context.beginPath()
    writeTxt('送 货 单', 'normal bold 30px', [370, 30])
    writeTxt('No', '20px', [450, 34])
    writeTxt('收货单地址:XXXXX', '14px', [12, 70], 'left')
    writeTxt('地     址:XXXXXXXXXXX', '14px', [12, 100], 'left')
    writeTxt('2022 年 9 月 22 日', '14px', [680, 100], 'right')
    writeTxt('收货单位及经手人(签章):', '14px', [12, 350], 'left')
    writeTxt('送货单位及经手人(签章):', '14px', [400, 350],)

    const imgList = [
        {
            // imgUrl: 'https://profile.csdnimg.cn/4/1/C/0_weixin_41277748',
            imgUrl: path.join(__dirname + '/reject.png'),
            position: [180, 350, 90, 80]
        },
        {
            imgUrl: path.join(__dirname + '/pass.png'),
            // imgUrl: 'https://profile.csdnimg.cn/4/1/C/0_weixin_41277748',
            position: [500, 350, 90, 80]
        },
    ]
    await drawImg(imgList)
    writeTxt('井底的蜗牛', '24px', [240, 370],)
    writeTxt('井底的蜗牛', '24px', [550, 370],)
    drawTable()

    const buffer = canvas.toBuffer("image/png")
    const imgPath = new Date().getTime() + '.png'
    let filPath = path.join(__dirname + '/static/', imgPath)
    //把图片写入static文件夹
    fs.writeFileSync(filPath, buffer)
    let file = fs.readFileSync(filPath)
    let mimeType = mime.lookup(filPath); //读取图片文件类型
    ctx.set('content-type', mimeType); //设置返回类型
    ctx.body = file; //返回图片
    context.clearRect(0, 0, width, height);
});
app.use(router.routes())
app.use(router.allowedMethods())

app.listen(3000)

文件中出现的图片

目录格式

启动 node index.js

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2022-09-30 00:43:36  更:2022-09-30 00:47:16 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 5:16:11-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码