我的编程空间,编程开发者的网络收藏夹
学习永远不晚

前端小票打印、网页打印(uniapp、小程序、ESC/POS指令)

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

前端小票打印、网页打印(uniapp、小程序、ESC/POS指令)

由于我在做项目期间遇到各种各样的打印,于是想写一篇文章来总结一下我遇到的打印需求以及解决方案。总的来说,目前我遇到的打印需求可以分为两类,网页打印和小票打印,在实现过程中,又可以分为前后端分别来实现,下面我就来说一下前端实现的方法和思路

目录

 一、小票打印

 打印指令封装

1.蓝牙打印机

2.网口打印机

打印小票

打印效果(这里仅为展示,非上述代码打印)

3.USB打印机

 二、网页打印

1.windows.print()

1.1使用媒体查询

1.2监听打印事件

1.3分页符

1.4设置纸张


 

 一、小票打印

目前市面上的小票打印机大多采用的打印指令集为ESC/POS指令,它可以使用ASCII码、十进制、十六进制来控制打印,我们可以使用它来控制字体大小、打印排版、字体加粗、下划线、走纸、切纸、控制钱箱等,下面以初始化打印机为例:

ASCII码  ESC  @十进制码  27  64十六进制  1B  40

小票打印纸的宽度一般可分58mm和80mm,这里指的是打印纸的宽度,但是在实际打印的时候,有效打印区域并没有这么宽。

 打印机纸宽58mm,页的宽度384,字符宽度为1,每行最多盛放32个字符 打印机纸宽80mm,页的宽度576,字符宽度为1,每行最多盛放48个字符

上面说的字符指的是打印到小票上的内容,其中数字和字母占1个字符,中文占2个字符,也就是说,如果使用58mm的打印纸,一行最多可以打印16个汉字或者32个数字 。

当然这是在不改变字体大小的情况下,如果我们改变了字体大小,那么一行盛放的内容也会改变。

//控制字符大小ASCII码   GS  !   n十进制码  29  33  n十六进制  1D  21  n

这里的n是一个变量, 0 ≤ n ≤ 255 

用二进制表示,n的取值范围就是00000000到11111111,其中二进制的前四位用来控制宽度,后四位用来控制高度。0000表示不变0001表示放大2倍0002表示放大3倍,以此类推

该命令对所有字符(英数字符和汉字) 有效。

缺省值:n = 0

 下面我们来看一下字符的不同放大倍数(这里的1倍,表示使用默认大小):

放大倍数n(二进制)n(十进制)
宽度1倍,高度1倍000000000
宽度1倍,高度2倍000000011
宽度1倍,高度3倍000000022
宽度2倍,高度1倍0001000016
宽度2倍,高度2倍0001000117
宽度2倍,高度3倍0001000218
宽度3倍,高度1倍0002000032
宽度3倍,高度2倍0002000133
宽度3倍,高度3倍0002000234

PS:打印纸时间有些长,字迹有些模糊,见谅

 打印指令封装

// 打印机纸宽58mm,页的宽度384,字符宽度为1,每行最多盛放32个字符// 打印机纸宽80mm,页的宽度576,字符宽度为1,每行最多盛放48个字符const PAGE_WIDTH = 576;const MAX_CHAR_COUNT_EACH_LINE = 48;//字符串转字节序列function stringToByte(str) {  var bytes = new Array();  var len, c;  len = str.length;  for (var i = 0; i < len; i++) {    c = str.charCodeAt(i);    if (c >= 0x010000 && c <= 0x10FFFF) {      bytes.push(((c >> 18) & 0x07) | 0xF0);      bytes.push(((c >> 12) & 0x3F) | 0x80);      bytes.push(((c >> 6) & 0x3F) | 0x80);      bytes.push((c & 0x3F) | 0x80);    } else if (c >= 0x000800 && c <= 0x00FFFF) {      bytes.push(((c >> 12) & 0x0F) | 0xE0);      bytes.push(((c >> 6) & 0x3F) | 0x80);      bytes.push((c & 0x3F) | 0x80);    } else if (c >= 0x000080 && c <= 0x0007FF) {      bytes.push(((c >> 6) & 0x1F) | 0xC0);      bytes.push((c & 0x3F) | 0x80);    } else {      bytes.push(c & 0xFF);    }  }  return bytes;}//字节序列转ASCII码//[0x24, 0x26, 0x28, 0x2A] ==> "$&C*"function byteToString(arr) {  if (typeof arr === 'string') {    return arr;  }  var str = '',    _arr = arr;  for (var i = 0; i < _arr.length; i++) {    var one = _arr[i].toString(2),      v = one.match(/^1+?(?=0)/);    if (v && one.length == 8) {      var bytesLength = v[0].length;      var store = _arr[i].toString(2).slice(7 - bytesLength);      for (var st = 1; st < bytesLength; st++) {        store += _arr[st + i].toString(2).slice(2);      }      str += String.fromCharCode(parseInt(store, 2));      i += bytesLength - 1;    } else {      str += String.fromCharCode(_arr[i]);    }  }  return str;}//居中function Center() {  var Center = [];  Center.push(27);  Center.push(97);  Center.push(1);  var strCenter = byteToString(Center);  return strCenter;}//居左function Left() {  var Left = [];  Left.push(27);  Left.push(97);  Left.push(0);  var strLeft = byteToString(Left);  return strLeft;}//居右function Right() {  var right = [];  Left.push(27);  Left.push(97);  Left.push(2);  var strRight = byteToString(right);  return strRight;}//标准字体function Size1() {  var Size1 = [];  Size1.push(29);  Size1.push(33);  Size1.push(0);  var strSize1 = byteToString(Size1);  return strSize1;}//大号字体function Size2(n) {  var Size2 = [];  Size2.push(29);  Size2.push(33);  Size2.push(n);  var strSize2 = byteToString(Size2);  return strSize2;}// 字体加粗function boldFontOn() {  var arr = []  arr.push(27)  arr.push(69)  arr.push(1)  var cmd = byteToString(arr);  return cmd}// 取消字体加粗function boldFontOff() {  var arr = []  arr.push(27)  arr.push(69)  arr.push(0)  var cmd = byteToString(arr);  return cmd}// 打印并走纸n行function feedLines(n = 1) {  var feeds = []  feeds.push(27)  feeds.push(100)  feeds.push(n)  var printFeedsLines = byteToString(feeds);  return printFeedsLines}// 切纸function cutPaper() {  var cut = []  cut.push(29)  cut.push(86)  cut.push(49)  var cutType = byteToString(cut);  return cutType}// 开钱箱function open_money_box() {  var open = []  open.push(27)  open.push(112)  open.push(0)  open.push(60)  open.push(255)  var openType = byteToString(open)  return openType}// 初始化打印机function init() {  var arr = []  arr.push(27)  arr.push(68)  arr.push(0)  var str = byteToString(arr)  return str}function setLeftMargin(len = 1) {  var arr = []  arr.push(29)  arr.push(76)  arr.push(len)  var str = byteToString(arr)  return str}// 设置打印区域宽度function setPrintAreaWidth(width) {  var arr = []  arr.push(29)  arr.push(87)  arr.push(width)  var str = byteToString(arr)  return str}function isChinese(str) {  return /^[\u4e00-\u9fa5]$/.test(str);}// str是否全含中文或者中文标点function isHaveChina(str) {  if (escape(str).indexOf("%u") < 0) {    return 0  } else {    return 1  }}function getStringWidth(str) {  let width = 0;  for (let i = 0, len = str.length; i < len; i++) {    width += isHaveChina(str.charAt(i)) ? 2 : 1;  }  return width;}function inline(str1, str2, fillWith = ' ', fontWidth = 1) {  const lineWidth = MAX_CHAR_COUNT_EACH_LINE / fontWidth;  // 需要填充的字符数量  let fillCount = lineWidth - (getStringWidth(str1) + getStringWidth(str2)) % lineWidth;  let fillStr = new Array(fillCount).fill(fillWith.charAt(0)).join('');  return str1 + fillStr + str2;}function fillLine(fillWith = '-', fontWidth = 1) {  const lineWidth = MAX_CHAR_COUNT_EACH_LINE / fontWidth;  return new Array(lineWidth).fill(fillWith.charAt(0)).join('');}function fillAround(str, fillWith = '-', fontWidth = 1) {  const lineWidth = MAX_CHAR_COUNT_EACH_LINE / fontWidth;  let strWidth = getStringWidth(str);  // 内容已经超过一行了,没必要填充  if (strWidth >= lineWidth) {    return str;  }  // 需要填充的字符数量  let fillCount = lineWidth - strWidth;  // 左侧填充的字符数量  let leftCount = Math.round(fillCount / 2);  // 两侧的填充字符,需要考虑左边需要填充,右边不需要填充的情况  let fillStr = new Array(leftCount).fill(fillWith.charAt(0)).join('');  return fillStr + str + fillStr.substr(0, fillCount - leftCount);}

也就是说,如果我们使用的打印机采用的是ESC/POS指令集(我这里使用过佳博、芯烨、斯普瑞特打印机),只要我们想办法把打印指令发送给打印机,打印机就可以识别到并且进行打印等操作。那么我们该如何发送呢?

1.蓝牙打印机

参考掘金 zgt_不梦的文章 微信小程序连接蓝牙打印机打印图片示例

  • 初始化蓝牙模块 wx.openBluetoothAdapter()
  • 初始化完成后搜寻附近的蓝牙设备 wx.startBluetoothDevicesDiscovery()
  • 监听寻找到新设备的事件 wx.onBluetoothDeviceFound()
  • 在监听寻找到新设备的事件回调中获取所有蓝牙设备列表 wx.getBluetoothDevices()
  • 连接低功耗蓝牙设备 wx.createBLEConnection()
  • 连接成功后获取蓝牙设备服务 wx.getBLEDeviceServices()
  • 在服务中取(notify=true || indicate=true) && write=true 的特征值的 uuid: wx.getBLEDeviceCharacteristics()
  • 完成后停止搜寻 wx.stopBluetoothDevicesDiscovery()
  • 向低功耗蓝牙设备特征值中写入二进制数据 wx.writeBLECharacteristicValue()
  • 离开页面时取消蓝牙连接 wx.closeBLEConnection()
  • 关闭蓝牙模块 wx.closeBluetoothAdapter()

亲测,好使!在uniapp也可以,只需替换对应的API即可

2.网口打印机

这里我使用的scoket连接,相比于USB打印,这里需要保证打印机和安卓设备在同一局域网下。好处是安卓设备可以和打印机距离较远(比如厨房打印)。这里以斯普瑞特打印机为例:斯普瑞特官网 https://www.sprinter.com.cn/https://www.sprinter.com.cn/在进行数据通信之前,我们需要知道打印机在此局域网下的 IP,下图为“一键配网”工具

 通过这个工具我们可以方便快捷的查询到打印机的IP,或者可以根据空闲的网段来修改默认分配的IP,斯普瑞特POS打印机的端口是9100。

如果是其他品牌的打印机,我们也可以使用arp命令来查看当前局域网下的IP

拿到打印机的IP之后我们怎么来测试一下打印机呢?

我们可以使用telnet命令(这个在Windows系统一般默认是关闭的,需要我们手动打开)

//telnet + 空格 + ip + 空格 + 端口号telnet 192.168.5.6 9100

打开命令行窗口输入telnet命令,按下回车

 如果端口关闭或者无法连接,则显示不能打开到主机的链接,链接失败;端口打开的情况下,链接成功,则进入telnet页面(全黑的),证明端口可用。

连接成功后,我们输入任何内容后,按下回车,打印机就会打印我们刚才输入的内容。

接下来我们要使用scoket来连接安卓设备和打印机,这里我使用的是uniapp

function tcpWrite(buffer, printerInfo) {  var Socket = plus.android.importClass("java.net.Socket");  var PrintWriter = plus.android.importClass("java.io.PrintWriter");  var BufferedWriter = plus.android.importClass("java.io.BufferedWriter");  var OutputStreamWriter = plus.android.importClass("java.io.OutputStreamWriter");  var BufferedReader = plus.android.importClass("java.io.BufferedReader");  var InputStreamReader = plus.android.importClass("java.io.InputStreamReader");  var InetSocketAddress = plus.android.importClass("java.net.InetSocketAddress");  //连接  注意:这里的端口一定是数字类型  var sk = null  try {    sk = new Socket(printerInfo.IP, Number(printerInfo.PORT));    sk.setSoTimeout(5000);  } catch (e) {    console.log(e, 'ee')    uni.showToast({      icon: 'none',      title: '打印机连接失败'    })  }  //发送  try {    var outputStreamWriter = new OutputStreamWriter(sk.getOutputStream(), "GBK");    var bufferWriter = new BufferedWriter(outputStreamWriter);    var out = new PrintWriter(bufferWriter, true);    out.println(buffer);    //关闭tcp连接    out.close();  } catch (e) {    console.log(e, 'ee')    uni.showToast({      icon: 'none',      title: '打印机数据传输失败'    })  }}

打印小票

目前我们已经可以开心的使用打印功能了,只需要组合一下打印指令即可。这里需要注意的是,如果我们在此之前设置了字符大小宽高均放大2倍,那么后面打印的字符都会被放大,所以如果后面我们想使用默认字符大小,我们还需要再次设置字符大小为默认来覆盖之前的指令

//这里的EscPosUtil.js就是上面封装的打印指令import Esc from './EscPosUtil.js';// 打印文字格式let strCenter = Esc.Center(); //文字居中let strLeft = Esc.Left(); //文字靠左let strSize1 = Esc.Size1(); //默认文字let strSize2 = Esc.Size2(17); //文字放大两倍(长宽均为两倍)let printerInfo = {  IP:'192.168.5.6',  PORT: 9100}let strCmd = strCenter + Esc.Size2(17) + Esc.boldFontOn() + '测试门店'+ "\n";  strCmd += strSize1 + Esc.fillLine(' ') + "\n"  strCmd += strCenter + Esc.Size2(17) + Esc.boldFontOn() + '结账单-堂食'  + "\n";  strCmd += strSize1 + Esc.fillLine(' ') + "\n"  strCmd += strLeft + Esc.Size2(17) + "取餐号:" + '62' + "\n";  strCmd += Esc.inline('桌号:' + '牡丹厅', '人数:' + '6', ' ', 2) + "\n"  strCmd += Esc.boldFontOff() + strSize1 + Esc.fillLine(' ') + "\n"  strCmd += strLeft + strSize1 + "订单号:" + '202305171749110001' + "\n";  // 商品信息  strCmd += Esc.fillAround('商品') + "\n"  // 票尾  strCmd += Esc.fillLine(' ') + "\n"  strCmd += strCenter + '欢迎下次光临!' + "\n";  strCmd += Esc.feedLines(4) + "\n"  // 切纸  strCmd += Esc.cutPaper()tcpWrite(strCmd, printerInfo)

打印效果(这里仅为展示,非上述代码打印)

3.USB打印机

这里我使用的是uniapp插件市场的插件,如果你了解安卓原生开发,你也可以自己制作一个原生插件,或者使用Native.js开发。使用原生插件在本地调试需要先打包“自定义调试基座”,在本地测试后再打正式包。

uni-app基于nativejs实现USB-OTG通讯 - 简书1,监听USB拔出连接,判断是否含有权限 2,获取权限后,打开设备实现连接 3,读写发送接受数据https://www.jianshu.com/p/7c308ffcd789

uni-app官网uni-app,uniCloud,serverlesshttps://uniapp.dcloud.net.cn/plugin/native-plugin.html

Android USB接口热敏小票打印机插件usbPrinter - DCloud 插件市场 本插件提供安卓手机通过USB接口连接热敏小票打印机进行打印的相关功能。通过USB连接相比使用蓝牙连接更稳定。https://ext.dcloud.net.cn/plugin?id=7757使用USB插件后,我们可以监听USB设备的插入和拔出,在初始化之后,我们可以进行数据通信,将上面封装的打印指令传给打印机即可

 二、网页打印

由于是网页运行在浏览器中,所以我们只能使用浏览器给我们提供的API

1.windows.print()

这个API在不同的浏览器中会有差异,其作用就是可以把网页中的body元素打印出来,如果我们不想打印整个body元素,则需要将body的innerHTML替换。使用这种方式有时有些页面样式会和打印出来的不一样,那么我们就要使用其他方式来优化。

//使用方法document.body.innerHTML = newstr;  // 把需要打印的指定内容赋给bodywindow.print();

1.1使用媒体查询

@media print {  //把需要打印时才用到的样式写到这里  p{    font-size:16px;  }}

同理,你也可以直接在CSS文件或者style标签中加上 media="print"

1.2监听打印事件

//监听打印之前的事件window.onbeforeprint = function() {  //可以修改元素样式}
//监听打印之后的事件window.onafterprint = function() {   //恢复之前的样式}

1.3分页符

  1.3.1 page-break-before  指定元素前插入分页符

  1.3.2 page-break-after  指定元素后插入分页符

page-break-before、page-break-after分页符属性
描述
auto默认。如果必要则在元素后插入分页符。
always在元素后插入分页符。
avoid避免在元素后插入分页符。
left在元素之后足够的分页符,一直到一张空白的左页为止。
right在元素之后足够的分页符,一直到一张空白的右页为止。
inherit规定应该从父元素继承 page-break-after 属性的设置。

1.您不能对绝对定位的元素使用此属性。

2.请尽可能少地使用分页属性,并且避免在表格、浮动元素、带有边框的块元素中使用分页属性。

3.任何版本的Internet Explorer(包括IE8)支持属性值"left","right",和"inherit"。

4.Firefox,Chrome和Safari不支持属性值"avoid","left"和"right"。.

@media print {    footer {page-break-after: always;}}

3 page-break-inside  设置是否在指定元素中插入分页符 

page-break-inside分页符属性
描述
auto默认。如果必要则在元素内部插入分页符。
avoid避免在元素内部插入分页符。
inherit规定应该从父元素继承 page-break-inside 属性的设置。
  1. 您不能对绝对定位的元素使用此属性。
  2. 请尽可能少地使用分页属性,并且避免在表格、浮动元素、带有边框的块元素中使用分页属性。
  3. IE8 及更早IE版本不支持 "inherit" 属性。
  4. Firefox, Chrome, 以及 Safari 不支持属性值 "avoid".
//避免在 
元素中插入分页符:@media print { pre, blockquote {page-break-inside: avoid;}}

1.4设置纸张

@page:  用来设置页面大小、边距、方向等

//portrait:纵向;  landscape: 横向@page {    size: A4 portrait;  //设置纸张及其方向    这里表示使用A4纸张,打印方向为纵向     margin: 3.7cm 2.6cm 3.5cm; //设置纸张外边距} // 去除页眉@page { margin-top: 0; } // 去除页脚@page { margin-bottom: 0; }

 值得注意的是,如果我们使用的打印机是黑白打印的,比如针式打印机,那么我们使用的颜色最好是#000,如果使用#999这种灰色,打印效果会很不清晰

 三、结尾

如果您有什么好的建议和想法,欢迎讨论

 

来源地址:https://blog.csdn.net/Linxi_001/article/details/130867268

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

前端小票打印、网页打印(uniapp、小程序、ESC/POS指令)

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

2023年最新最全uniCloud入门学习,零基础入门到实战项目 uni-admin打造uniapp网页后端 微信支付宝抖音小程序后端 unicloud数据后台快速打造uniapp小程序项目

今天开始带着大家一起零基础学习uniCloud,在下面的课程中我们就简称uniCloud为cloud吧。我这里从零基础开始教大家,后面可以带大家简单的做一个实战项目。所以不用担心自己没有基础,跟着石头哥认真学习就行了的。 一,认识un
2023-08-23

编程热搜

  • Android:VolumeShaper
    VolumeShaper(支持版本改一下,minsdkversion:26,android8.0(api26)进一步学习对声音的编辑,可以让音频的声音有变化的播放 VolumeShaper.Configuration的三个参数 durati
    Android:VolumeShaper
  • Android崩溃异常捕获方法
    开发中最让人头疼的是应用突然爆炸,然后跳回到桌面。而且我们常常不知道这种状况会何时出现,在应用调试阶段还好,还可以通过调试工具的日志查看错误出现在哪里。但平时使用的时候给你闹崩溃,那你就欲哭无泪了。 那么今天主要讲一下如何去捕捉系统出现的U
    Android崩溃异常捕获方法
  • android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
    系统的设置–>电池–>使用情况中,统计的能耗的使用情况也是以power_profile.xml的value作为基础参数的1、我的手机中power_profile.xml的内容: HTC t328w代码如下:
    android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
  • Android SQLite数据库基本操作方法
    程序的最主要的功能在于对数据进行操作,通过对数据进行操作来实现某个功能。而数据库就是很重要的一个方面的,Android中内置了小巧轻便,功能却很强的一个数据库–SQLite数据库。那么就来看一下在Android程序中怎么去操作SQLite数
    Android SQLite数据库基本操作方法
  • ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
    工作的时候为了方便直接打开编辑文件,一些常用的软件或者文件我们会放在桌面,但是在ubuntu20.04下直接直接拖拽文件到桌面根本没有效果,在进入桌面后发现软件列表中的软件只能收藏到面板,无法复制到桌面使用,不知道为什么会这样,似乎并不是很
    ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
  • android获取当前手机号示例程序
    代码如下: public String getLocalNumber() { TelephonyManager tManager =
    android获取当前手机号示例程序
  • Android音视频开发(三)TextureView
    简介 TextureView与SurfaceView类似,可用于显示视频或OpenGL场景。 与SurfaceView的区别 SurfaceView不能使用变换和缩放等操作,不能叠加(Overlay)两个SurfaceView。 Textu
    Android音视频开发(三)TextureView
  • android获取屏幕高度和宽度的实现方法
    本文实例讲述了android获取屏幕高度和宽度的实现方法。分享给大家供大家参考。具体分析如下: 我们需要获取Android手机或Pad的屏幕的物理尺寸,以便于界面的设计或是其他功能的实现。下面就介绍讲一讲如何获取屏幕的物理尺寸 下面的代码即
    android获取屏幕高度和宽度的实现方法
  • Android自定义popupwindow实例代码
    先来看看效果图:一、布局
  • Android第一次实验
    一、实验原理 1.1实验目标 编程实现用户名与密码的存储与调用。 1.2实验要求 设计用户登录界面、登录成功界面、用户注册界面,用户注册时,将其用户名、密码保存到SharedPreference中,登录时输入用户名、密码,读取SharedP
    Android第一次实验

目录