翼度科技»论坛 编程开发 JavaScript 查看内容

记录--前端小票打印、网页打印

6

主题

6

帖子

18

积分

新手上路

Rank: 1

积分
18
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助



 一、小票打印

目前市面上的小票打印机大多采用的打印指令集为ESC/POS指令,它可以使用ASCII码、十进制、十六进制来控制打印,我们可以使用它来控制字体大小、打印排版、字体加粗、下划线、走纸、切纸、控制钱箱等,下面以初始化打印机为例:
  1. ASCII码  ESC  @
  2. 十进制码  27  64
  3. 十六进制  1B  40
复制代码
小票打印纸的宽度一般可分58mm和80mm,这里指的是打印纸的宽度,但是在实际打印的时候,有效打印区域并没有这么宽。
  1. 打印机纸宽58mm,页的宽度384,字符宽度为1,每行最多盛放32个字符
  2. 打印机纸宽80mm,页的宽度576,字符宽度为1,每行最多盛放48个字符
复制代码
上面说的字符指的是打印到小票上的内容,其中数字和字母占1个字符,中文占2个字符,也就是说,如果使用58mm的打印纸,一行最多可以打印16个汉字或者32个数字 。
当然这是在不改变字体大小的情况下,如果我们改变了字体大小,那么一行盛放的内容也会改变。
  1. //控制字符大小
  2. ASCII码   GS  !   n
  3. 十进制码  29  33  n
  4. 十六进制  1D  21  n
复制代码
1.这里的n是一个变量, 0 ≤ n ≤ 255 
2.用二进制表示,n的取值范围就是00000000到11111111,其中二进制的前四位用来控制宽度,后四位用来控制高度。0000表示不变0001表示放大2倍0002表示放大3倍,以此类推
3.该命令对所有字符(英数字符和汉字) 有效。
4.缺省值: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:打印纸时间有些长,字迹有些模糊,见谅
 打印指令封装
  1. // 打印机纸宽58mm,页的宽度384,字符宽度为1,每行最多盛放32个字符
  2. // 打印机纸宽80mm,页的宽度576,字符宽度为1,每行最多盛放48个字符
  3. const PAGE_WIDTH = 576;
  4. const MAX_CHAR_COUNT_EACH_LINE = 48;
  5. //字符串转字节序列
  6. function stringToByte(str) {
  7.   var bytes = new Array();
  8.   var len, c;
  9.   len = str.length;
  10.   for (var i = 0; i < len; i++) {
  11.     c = str.charCodeAt(i);
  12.     if (c >= 0x010000 && c <= 0x10FFFF) {
  13.       bytes.push(((c >> 18) & 0x07) | 0xF0);
  14.       bytes.push(((c >> 12) & 0x3F) | 0x80);
  15.       bytes.push(((c >> 6) & 0x3F) | 0x80);
  16.       bytes.push((c & 0x3F) | 0x80);
  17.     } else if (c >= 0x000800 && c <= 0x00FFFF) {
  18.       bytes.push(((c >> 12) & 0x0F) | 0xE0);
  19.       bytes.push(((c >> 6) & 0x3F) | 0x80);
  20.       bytes.push((c & 0x3F) | 0x80);
  21.     } else if (c >= 0x000080 && c <= 0x0007FF) {
  22.       bytes.push(((c >> 6) & 0x1F) | 0xC0);
  23.       bytes.push((c & 0x3F) | 0x80);
  24.     } else {
  25.       bytes.push(c & 0xFF);
  26.     }
  27.   }
  28.   return bytes;
  29. }
  30. //字节序列转ASCII码
  31. //[0x24, 0x26, 0x28, 0x2A] ==> "$&C*"
  32. function byteToString(arr) {
  33.   if (typeof arr === 'string') {
  34.     return arr;
  35.   }
  36.   var str = '',
  37.     _arr = arr;
  38.   for (var i = 0; i < _arr.length; i++) {
  39.     var one = _arr[i].toString(2),
  40.       v = one.match(/^1+?(?=0)/);
  41.     if (v && one.length == 8) {
  42.       var bytesLength = v[0].length;
  43.       var store = _arr[i].toString(2).slice(7 - bytesLength);
  44.       for (var st = 1; st < bytesLength; st++) {
  45.         store += _arr[st + i].toString(2).slice(2);
  46.       }
  47.       str += String.fromCharCode(parseInt(store, 2));
  48.       i += bytesLength - 1;
  49.     } else {
  50.       str += String.fromCharCode(_arr[i]);
  51.     }
  52.   }
  53.   return str;
  54. }
  55. //居中
  56. function Center() {
  57.   var Center = [];
  58.   Center.push(27);
  59.   Center.push(97);
  60.   Center.push(1);
  61.   var strCenter = byteToString(Center);
  62.   return strCenter;
  63. }
  64. //居左
  65. function Left() {
  66.   var Left = [];
  67.   Left.push(27);
  68.   Left.push(97);
  69.   Left.push(0);
  70.   var strLeft = byteToString(Left);
  71.   return strLeft;
  72. }
  73. //居右
  74. function Right() {
  75.   var right = [];
  76.   Left.push(27);
  77.   Left.push(97);
  78.   Left.push(2);
  79.   var strRight = byteToString(right);
  80.   return strRight;
  81. }
  82. //标准字体
  83. function Size1() {
  84.   var Size1 = [];
  85.   Size1.push(29);
  86.   Size1.push(33);
  87.   Size1.push(0);
  88.   var strSize1 = byteToString(Size1);
  89.   return strSize1;
  90. }
  91. //大号字体
  92. /*  放大1倍  n = 0
  93. *  长宽各放大2倍  n = 17 */
  94. function Size2(n) {
  95.   var Size2 = [];
  96.   Size2.push(29);
  97.   Size2.push(33);
  98.   Size2.push(n);
  99.   var strSize2 = byteToString(Size2);
  100.   return strSize2;
  101. }
  102. // 字体加粗
  103. function boldFontOn() {
  104.   var arr = []
  105.   arr.push(27)
  106.   arr.push(69)
  107.   arr.push(1)
  108.   var cmd = byteToString(arr);
  109.   return cmd
  110. }
  111. // 取消字体加粗
  112. function boldFontOff() {
  113.   var arr = []
  114.   arr.push(27)
  115.   arr.push(69)
  116.   arr.push(0)
  117.   var cmd = byteToString(arr);
  118.   return cmd
  119. }
  120. // 打印并走纸n行
  121. function feedLines(n = 1) {
  122.   var feeds = []
  123.   feeds.push(27)
  124.   feeds.push(100)
  125.   feeds.push(n)
  126.   var printFeedsLines = byteToString(feeds);
  127.   return printFeedsLines
  128. }
  129. // 切纸
  130. function cutPaper() {
  131.   var cut = []
  132.   cut.push(29)
  133.   cut.push(86)
  134.   cut.push(49)
  135.   var cutType = byteToString(cut);
  136.   return cutType
  137. }
  138. // 开钱箱
  139. function open_money_box() {
  140.   var open = []
  141.   open.push(27)
  142.   open.push(112)
  143.   open.push(0)
  144.   open.push(60)
  145.   open.push(255)
  146.   var openType = byteToString(open)
  147.   return openType
  148. }
  149. // 初始化打印机
  150. function init() {
  151.   var arr = []
  152.   arr.push(27)
  153.   arr.push(68)
  154.   arr.push(0)
  155.   var str = byteToString(arr)
  156.   return str
  157. }
  158. /*
  159. 设置左边距
  160. len:
  161. */
  162. function setLeftMargin(len = 1) {
  163.   var arr = []
  164.   arr.push(29)
  165.   arr.push(76)
  166.   arr.push(len)
  167.   var str = byteToString(arr)
  168.   return str
  169. }
  170. // 设置打印区域宽度
  171. function setPrintAreaWidth(width) {
  172.   var arr = []
  173.   arr.push(29)
  174.   arr.push(87)
  175.   arr.push(width)
  176.   var str = byteToString(arr)
  177.   return str
  178. }
  179. /**
  180. * @param str
  181. * @returns {boolean} str是否全是中文
  182. */
  183. function isChinese(str) {
  184.   return /^[\u4e00-\u9fa5]$/.test(str);
  185. }
  186. // str是否全含中文或者中文标点
  187. function isHaveChina(str) {
  188.   if (escape(str).indexOf("%u") < 0) {
  189.     return 0
  190.   } else {
  191.     return 1
  192.   }
  193. }
  194. /**
  195. * 返回字符串宽度(1个中文=2个英文字符)
  196. * @param str
  197. * @returns {number}
  198. */
  199. function getStringWidth(str) {
  200.   let width = 0;
  201.   for (let i = 0, len = str.length; i < len; i++) {
  202.     width += isHaveChina(str.charAt(i)) ? 2 : 1;
  203.   }
  204.   return width;
  205. }
  206. /**
  207. * 同一行输出str1, str2,str1居左, str2居右
  208. * @param {string} str1 内容1
  209. * @param {string} str2 内容2
  210. * @param {string} fillWith str1 str2之间的填充字符
  211. * @param {number} fontWidth 字符宽度 1/2
  212. *
  213. */
  214. function inline(str1, str2, fillWith = ' ', fontWidth = 1) {
  215.   const lineWidth = MAX_CHAR_COUNT_EACH_LINE / fontWidth;
  216.   // 需要填充的字符数量
  217.   let fillCount = lineWidth - (getStringWidth(str1) + getStringWidth(str2)) % lineWidth;
  218.   let fillStr = new Array(fillCount).fill(fillWith.charAt(0)).join('');
  219.   return str1 + fillStr + str2;
  220. }
  221. /**
  222. * 用字符填充一整行
  223. * @param {string} fillWith 填充字符
  224. * @param {number} fontWidth 字符宽度 1/2
  225. */
  226. function fillLine(fillWith = '-', fontWidth = 1) {
  227.   const lineWidth = MAX_CHAR_COUNT_EACH_LINE / fontWidth;
  228.   return new Array(lineWidth).fill(fillWith.charAt(0)).join('');
  229. }
  230. /**
  231. * 文字内容居中,左右用字符填充
  232. * @param {string} str 文字内容
  233. * @param {number} fontWidth 字符宽度 1/2
  234. * @param {string} fillWith str1 str2之间的填充字符
  235. */
  236. function fillAround(str, fillWith = '-', fontWidth = 1) {
  237.   const lineWidth = MAX_CHAR_COUNT_EACH_LINE / fontWidth;
  238.   let strWidth = getStringWidth(str);
  239.   // 内容已经超过一行了,没必要填充
  240.   if (strWidth >= lineWidth) {
  241.     return str;
  242.   }
  243.   // 需要填充的字符数量
  244.   let fillCount = lineWidth - strWidth;
  245.   // 左侧填充的字符数量
  246.   let leftCount = Math.round(fillCount / 2);
  247.   // 两侧的填充字符,需要考虑左边需要填充,右边不需要填充的情况
  248.   let fillStr = new Array(leftCount).fill(fillWith.charAt(0)).join('');
  249.   return fillStr + str + fillStr.substr(0, fillCount - leftCount);
  250. }
复制代码
也就是说,如果我们使用的打印机采用的是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打印,这里需要保证打印机和安卓设备在同一局域网下。好处是安卓设备可以和打印机距离较远(比如厨房打印)。这里以斯普瑞特打印机为例:[斯普瑞特官网 www.sprinter.com.cn/
在进行数据通信之前,我们需要知道打印机在此局域网下的 IP, 下图为“一键配网”工具
 通过这个工具我们可以方便快捷的查询到打印机的IP,或者可以根据空闲的网段来修改默认分配的IP,斯普瑞特POS打印机的端口是9100。

如果是其他品牌的打印机,我们也可以使用arp命令来查看当前局域网下的IP
拿到打印机的IP之后我们怎么来测试一下打印机呢?
我们可以使用telnet命令(这个在Windows系统一般默认是关闭的,需要我们手动打开)
  1. //telnet + 空格 + ip + 空格 + 端口号
  2. telnet 192.168.5.6 9100
复制代码
打开命令行窗口输入telnet命令,按下回车

 如果端口关闭或者无法连接,则显示不能打开到主机的链接,链接失败;端口打开的情况下,链接成功,则进入telnet页面(全黑的),证明端口可用。
连接成功后,我们输入任何内容后,按下回车,打印机就会打印我们刚才输入的内容。
接下来我们要使用scoket来连接安卓设备和打印机,这里我使用的是uniapp
  1. /**
  2. * 调用tcp通信进行打印
  3. * @param {buffer}  buffer       打印数据
  4. * @param {object}  printerInfo  打印机对象{IP:'',PORT:''}
  5. */
  6. function tcpWrite(buffer, printerInfo) {
  7.   var Socket = plus.android.importClass("java.net.Socket");
  8.   var PrintWriter = plus.android.importClass("java.io.PrintWriter");
  9.   var BufferedWriter = plus.android.importClass("java.io.BufferedWriter");
  10.   var OutputStreamWriter = plus.android.importClass("java.io.OutputStreamWriter");
  11.   var BufferedReader = plus.android.importClass("java.io.BufferedReader");
  12.   var InputStreamReader = plus.android.importClass("java.io.InputStreamReader");
  13.   var InetSocketAddress = plus.android.importClass("java.net.InetSocketAddress");
  14.   //连接  注意:这里的端口一定是数字类型
  15.   var sk = null
  16.   try {
  17.     sk = new Socket(printerInfo.IP, Number(printerInfo.PORT));
  18.     sk.setSoTimeout(5000);
  19.   } catch (e) {
  20.     console.log(e, 'ee')
  21.     uni.showToast({
  22.       icon: 'none',
  23.       title: '打印机连接失败'
  24.     })
  25.   }
  26.   //发送
  27.   try {
  28.     var outputStreamWriter = new OutputStreamWriter(sk.getOutputStream(), "GBK");
  29.     var bufferWriter = new BufferedWriter(outputStreamWriter);
  30.     var out = new PrintWriter(bufferWriter, true);
  31.     out.println(buffer);
  32.     //关闭tcp连接
  33.     out.close();
  34.   } catch (e) {
  35.     console.log(e, 'ee')
  36.     uni.showToast({
  37.       icon: 'none',
  38.       title: '打印机数据传输失败'
  39.     })
  40.   }
  41. }
复制代码
打印小票

目前我们已经可以开心的使用打印功能了,只需要组合一下打印指令即可。这里需要注意的是,如果我们在此之前设置了字符大小宽高均放大2倍,那么后面打印的字符都会被放大,所以如果后面我们想使用默认字符大小,我们还需要再次设置字符大小为默认来覆盖之前的指令
  1. //这里的EscPosUtil.js就是上面封装的打印指令
  2. import Esc from './EscPosUtil.js';
  3. // 打印文字格式
  4. let strCenter = Esc.Center(); //文字居中
  5. let strLeft = Esc.Left(); //文字靠左
  6. let strSize1 = Esc.Size1(); //默认文字
  7. let strSize2 = Esc.Size2(17); //文字放大两倍(长宽均为两倍)
  8. let printerInfo = {
  9.   IP:'192.168.5.6',
  10.   PORT: 9100
  11. }
  12. let strCmd = strCenter + Esc.Size2(17) + Esc.boldFontOn() + '测试门店'+ "\n";
  13.   strCmd += strSize1 + Esc.fillLine(' ') + "\n"
  14.   strCmd += strCenter + Esc.Size2(17) + Esc.boldFontOn() + '结账单-堂食'  + "\n";
  15.   strCmd += strSize1 + Esc.fillLine(' ') + "\n"
  16.   strCmd += strLeft + Esc.Size2(17) + "取餐号:" + '62' + "\n";
  17.   strCmd += Esc.inline('桌号:' + '牡丹厅', '人数:' + '6', ' ', 2) + "\n"
  18.   strCmd += Esc.boldFontOff() + strSize1 + Esc.fillLine(' ') + "\n"
  19.   strCmd += strLeft + strSize1 + "订单号:" + '202305171749110001' + "\n";
  20.   // 商品信息
  21.   strCmd += Esc.fillAround('商品') + "\n"
  22.   // 票尾
  23.   strCmd += Esc.fillLine(' ') + "\n"
  24.   strCmd += strCenter + '欢迎下次光临!' + "\n";
  25.   strCmd += Esc.feedLines(4) + "\n"
  26.   // 切纸
  27.   strCmd += Esc.cutPaper()
  28. 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,serverless
https://uniapp.dcloud.net.cn/plugin/native-plugin.html#%E6%9C%AC%E5%9C%B0%E6%8F%92%E4%BB%B6-%E9%9D%9E%E5%86%85%E7%BD%AE%E5%8E%9F%E7%94%9F%E6%8F%92%E4%BB%B6

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替换。使用这种方式有时有些页面样式会和打印出来的不一样,那么我们就要使用其他方式来优化。
  1. //使用方法
  2. document.body.innerHTML = newstr;  // 把需要打印的指定内容赋给body
  3. window.print();
复制代码
1.1使用媒体查询
  1. @media print {
  2.   //把需要打印时才用到的样式写到这里
  3.   p{
  4.     font-size:16px;
  5.   }
  6. }
复制代码
同理,你也可以直接在CSS文件或者style标签中加上 media="print"
  1. [/code][size=4]1.2监听打印事件[/size]
  2. [code]//监听打印之前的事件
  3. window.onbeforeprint = function() {
  4.   //可以修改元素样式
  5. }
  6. //监听打印之后的事件
  7. window.onafterprint = function() {
  8.    //恢复之前的样式
  9. }
复制代码
1.3分页符

  1.3.1 page-break-before  指定元素前插入分页符
  1.3.2 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"。.
  1. @media print {
  2.     footer {page-break-after: always;}
  3. }
复制代码
1.3.3 page-break-inside  设置是否在指定元素中插入分页符 
值描述auto默认。如果必要则在元素内部插入分页符。avoid避免在元素内部插入分页符。inherit规定应该从父元素继承 page-break-inside 属性的设置。

  • 您不能对绝对定位的元素使用此属性。
  • 请尽可能少地使用分页属性,并且避免在表格、浮动元素、带有边框的块元素中使用分页属性。
  • IE8 及更早IE版本不支持 "inherit" 属性。
  • Firefox, Chrome, 以及 Safari 不支持属性值 "avoid".
  1. //避免在 <pre> 与 <blockquote> 元素中插入分页符:
  2. @media print {
  3.     pre, blockquote {page-break-inside: avoid;}
  4. }
复制代码
1.4设置纸张

@page:  用来设置页面大小、边距、方向等
  1. //portrait:纵向;  landscape: 横向
  2. @page {
  3.     size: A4 portrait;  //设置纸张及其方向    这里表示使用A4纸张,打印方向为纵向
  4.     margin: 3.7cm 2.6cm 3.5cm; //设置纸张外边距
  5. }
  6. // 去除页眉
  7. @page { margin-top: 0; }
  8. // 去除页脚
  9. @page { margin-bottom: 0; }
复制代码
 值得注意的是,如果我们使用的打印机是黑白打印的,比如针式打印机,那么我们使用的颜色最好是 #000,如果使用 #999这种灰色,打印效果会很不清晰
本文转载于:

https://juejin.cn/post/7237316724739457061

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

 


来源:https://www.cnblogs.com/smileZAZ/p/17435607.html
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具