// 网口打印机管理(80mm 终极美化版 · 无多余间距最终版) import net from 'net' import iconv from 'iconv-lite' import dayjs from 'dayjs' // ======================== 全局工具函数 ======================== const getPrintWidth = (str) => { let width = 0 const s = String(str || '') for (let i = 0; i < s.length; i++) { const char = s.charAt(i) if (/[\u4e00-\u9fa5\u3000-\u303f\uff00-\uffef]/.test(char)) { width += 2 } else { width += 1 } } return width } const padLeftAlign = (str, targetWidth) => { const s = String(str || '') const currentWidth = getPrintWidth(s) const spaceNum = Math.max(0, targetWidth - currentWidth) return s + ' '.repeat(spaceNum) } const padRightAlign = (str, targetWidth) => { const s = String(str || '') const currentWidth = getPrintWidth(s) const spaceNum = Math.max(0, targetWidth - currentWidth) return ' '.repeat(spaceNum) + s } const truncateByPrintWidth = (str, maxWidth) => { const s = String(str || '') let width = 0 let result = '' for (const char of s) { const charWidth = /[\u4e00-\u9fa5\u3000-\u303f\uff00-\uffef]/.test(char) ? 2 : 1 if (width + charWidth > maxWidth) break result += char width += charWidth } return result } const centerAlign = (str, lineWidth = 48) => { const s = String(str || '').trim(); // 去除首尾空格,避免干扰 const currentWidth = getPrintWidth(s); if (currentWidth >= lineWidth) return s; // 内容超宽时直接返回,不居中 const totalPad = lineWidth - currentWidth; const leftPad = Math.floor(totalPad / 2); const rightPad = totalPad - leftPad; // 避免奇数宽度时总长度超行宽 // 用空格填充左右,保证总宽度严格等于 lineWidth return ' '.repeat(leftPad) + s + ' '.repeat(rightPad); }; const createDivider = (lineWidth = 48, type = 'full') => { const fullChar = '═' const thinChar = '─' const char = type === 'full' ? fullChar : thinChar return char.repeat(Math.ceil(lineWidth / 2)).substring(0, lineWidth) } const setupPrinter = (socket, options = {}) => { socket.write(Buffer.from([0x1B, 0x40])) if (options.fontSize) { socket.write(Buffer.from([0x1B, 0x21, options.fontSize])) } if (options.bold) { socket.write(Buffer.from([0x1B, 0x45, options.bold ? 0x01 : 0x00])) } } // 用餐模式 堂食 dine-in 外带 take-out 外卖 take-away const dineModeFilter = t => { if (t === 'dine-in') return '堂食' if (t === 'take-out') return '外带' if (t === 'take-away') return '外卖' return t } // 支付类型 主扫 main_scan // 被扫 back_scan // 微信小程序 wechat_mini // 支付宝小程序 alipay_mini // 会员支付 vip_pay // 现金支付 cash_pay // 挂账支付 credit_pay const payTypeFilter = t => { if (t === 'main_scan') return '主扫' if (t === 'back_scan') return '被扫' if (t === 'wechat_mini') return '微信小程序' if (t === 'alipay_mini') return '支付宝小程序' if (t === 'vip_pay') return '余额支付' if (t === 'cash_pay') return '现金支付' if (t === 'credit_pay') return '挂账支付' return t } const formatAmount = (amount) => Number(amount || 0).toFixed(2) const timeFormt = 'YYYY/M/D HH:mm:ss' // ======================== 打印结算小票 ======================== export function ORDER(printerIp, order) { return new Promise((resolve) => { const socket = new net.Socket() const lineWidth = 48 socket.setTimeout(8000) socket.connect(9100, printerIp, () => { try { setupPrinter(socket) // ======================== 标题区域(彻底修复居中问题!) ======================== const title1 = order.shop_name || ''; let title2 = `结算单 #${order.orderInfo.orderNum}` if (order.isBefore) { title2 = `${order.isBefore ? '预' : ''}结算单 #${order.orderInfo.orderNum}`; } if (order.isGuest) { title2 = `客看单 #${order.orderInfo.orderNum}`; } // 核心修复:先写入空行清空打印机缓冲区,避免残留字符干扰居中起始位置 socket.write(iconv.encode('\n', 'gbk')); // 步骤1:重置打印机格式(避免残留格式影响) socket.write(Buffer.from([0x1B, 0x40])); // 步骤2:居中对齐标题 socket.write(Buffer.from([0x1B, 0x61, 0x01])); // 1 = center // 步骤3:设置标题1的格式(加粗+大号字体) socket.write(Buffer.from([0x1B, 0x21, 0x11])); // 字号放大(0x11 是常用放大值) socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗 socket.write(iconv.encode(title1 + '\n', 'gbk')); // 步骤4:恢复格式,打印标题2(常规字体) socket.write(Buffer.from([0x1B, 0x21, 0x00])); // 恢复默认字号 socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗 socket.write(iconv.encode(title2 + '\n\n', 'gbk')); socket.write(Buffer.from([0x1B, 0x61, 0x00])); // 0 = left // ======================== 订单信息(修复显示+换行问题) ======================== const tableLine = padLeftAlign('桌台号:', 8) + padLeftAlign(order.orderInfo.tableName || '无', 14) + '\n' socket.write(Buffer.from([0x1B, 0x21, 0x00])); // 正常字号 socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗 socket.write(iconv.encode(tableLine, 'gbk')); socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗 let orderInfo = '' // 修复:打印机-用餐模式 + 就餐人数 展示逻辑 const dineModeText = dineModeFilter(order.orderInfo.dineMode) // 1. 拼接打印机+用餐模式文本,空值兜底 const printerDineText = `${order.deviceName || '未知打印机'}-${dineModeText || '未知模式'}` // 2. 截断超长文本(左侧分配20宽度,保证不超限) const truncatedPrinterDine = truncateByPrintWidth(printerDineText, 20) // 3. 左侧左对齐填充20宽度(保证占满分配空间,不浪费空白) const leftPart = padLeftAlign(truncatedPrinterDine, 20) // 4. 右侧就餐人数:空值兜底为“无”,右对齐填充28宽度(20+28=48,刚好行宽) const seatNumText = order.orderInfo.seatNum || '0' const rightPart = padRightAlign(`${seatNumText}人`, 28) // 加标签更清晰,也可只显示数字 orderInfo += leftPart + rightPart + '\n' if (!order.isBefore && !order.isGuest) { orderInfo += padLeftAlign('结账时间:', 10) + padLeftAlign(order.orderInfo.paidTime || '-', 38) + '\n' } orderInfo += createDivider(lineWidth, 'thin') + '\n' // ======================== 商品表格 ======================== orderInfo += padLeftAlign('品名', 18) orderInfo += padLeftAlign('单价', 8) orderInfo += padLeftAlign('数量', 6) orderInfo += padRightAlign('小计', 16) + '\n' orderInfo += createDivider(lineWidth, 'thin') + '\n' // 处理商品列表(套餐主项显示金额,子项不显示金额 + 规格换行) order.carts.forEach(item => { if (item.number > 0) { // 套餐商品逻辑 if (item.proGroupInfo) { // 套餐主项:正常显示 名称、单价、数量、小计 const packageName = truncateByPrintWidth(item.name || '未知套餐', 18) orderInfo += padLeftAlign(packageName, 18) orderInfo += padLeftAlign(formatAmount(item.salePrice), 8) orderInfo += padLeftAlign(item.number || 1, 6) orderInfo += padRightAlign(formatAmount(item.totalAmount), 16) + '\n' // 套餐子项:只显示名称+规格,单价、小计全部为空 const proGroupInfo = item.proGroupInfo || [] proGroupInfo.forEach(subItem => { const subName = truncateByPrintWidth(`>${subItem.proName || '未知商品'}`, 18) orderInfo += padLeftAlign(subName, 18) orderInfo += padLeftAlign('', 8) // 子项单价空 orderInfo += padLeftAlign(subItem.number || 1, 6) orderInfo += padRightAlign('', 16) + '\n' // 子项小计空 // 子项规格换行显示 if (subItem.skuName) { const skuText = truncateByPrintWidth(` 规格:${subItem.skuName}`, 18) orderInfo += padLeftAlign(skuText, 18) orderInfo += padLeftAlign('', 8) orderInfo += padLeftAlign('', 6) orderInfo += padRightAlign('', 16) + '\n' } }) } else { // 普通商品:正常显示 + 规格换行 let productName = truncateByPrintWidth(item.name || '未知商品', 18) orderInfo += padLeftAlign(productName, 18) orderInfo += padLeftAlign(item.salePrice ? formatAmount(item.salePrice) : '', 8) orderInfo += padLeftAlign(item.number || 1, 6) orderInfo += padRightAlign(formatAmount(item.totalAmount), 16) + '\n' // 规格单独换行 if (item.skuName) { const skuLine = truncateByPrintWidth(` 规格:${item.skuName}`, 18) orderInfo += padLeftAlign(skuLine, 18) orderInfo += padLeftAlign('', 8) orderInfo += padLeftAlign('', 6) orderInfo += padRightAlign('', 16) + '\n' } } } }) orderInfo += createDivider(lineWidth, 'thin') + '\n' orderInfo += padLeftAlign('原价', 10) + padRightAlign(formatAmount(order.originAmount), 38) + '\n' orderInfo += padLeftAlign('优惠金额', 10) + padRightAlign(`-${formatAmount(order.discountAllAmount)}`, 38) + '\n' orderInfo += padLeftAlign('备注:', 6) + padLeftAlign(order.remark || '无', 38) + '\n' orderInfo += createDivider(lineWidth, 'thin') + '\n' socket.write(iconv.encode(orderInfo, 'gbk')) // ======================== 付款信息 ======================== if (order.isBefore) { const payableLine = padLeftAlign('应付', 10) + padRightAlign(formatAmount(order.originAmount), 52) + '\n' socket.write(Buffer.from([0x1B, 0x21, 0x11])); // 放大字号 socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗 socket.write(iconv.encode(payableLine, 'gbk')); socket.write(Buffer.from([0x1B, 0x21, 0x00])); // 恢复默认字号 socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗 socket.write(iconv.encode(createDivider(lineWidth, 'thin') + '\n', 'gbk')); } if (!order.isBefore && !order.isGuest) { const payableLine = padLeftAlign('应付', 10) + padRightAlign(formatAmount(order.orderInfo.payAmount), 52) + '\n' socket.write(Buffer.from([0x1B, 0x21, 0x11])); // 放大字号 socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗 socket.write(iconv.encode(payableLine, 'gbk')); socket.write(Buffer.from([0x1B, 0x21, 0x00])); // 恢复默认字号 socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗 socket.write(iconv.encode(createDivider(lineWidth, 'thin') + '\n', 'gbk')); const paidLine = padLeftAlign('已付', 10) + padRightAlign(formatAmount(order.orderInfo.payAmount), 52) + '\n' socket.write(Buffer.from([0x1B, 0x21, 0x11])); socket.write(Buffer.from([0x1B, 0x45, 0x01])); socket.write(iconv.encode(paidLine, 'gbk')); socket.write(Buffer.from([0x1B, 0x21, 0x00])); socket.write(Buffer.from([0x1B, 0x45, 0x00])); socket.write(iconv.encode(createDivider(lineWidth, 'thin') + '\n', 'gbk')); } // ======================== 底部 ======================== let bottom = '' if (!order.isBefore && !order.isGuest) { bottom += padLeftAlign('支付方式:', 8) + padLeftAlign(payTypeFilter(order.orderInfo.payType) || '无', 38) + '\n' } bottom += padLeftAlign('操作员:', 8) + padLeftAlign(order.loginAccount || '无', 38) + '\n' bottom += padLeftAlign('打印时间:', 10) + padLeftAlign(new Date().toLocaleString(), 38) + '\n' bottom += padLeftAlign('订单号:', 8) + padLeftAlign(order.orderInfo.orderNo || '-', 38) + '\n' bottom += createDivider(lineWidth, 'full') + '\n' bottom += '\n\n' bottom += '\n\n' socket.write(iconv.encode(bottom, 'gbk')) socket.write(Buffer.from([0x1D, 0x56, 0x00])) setTimeout(() => { socket.end() resolve('success') }, 400) } catch (err) { console.error(err) socket.destroy() resolve('error') } }) socket.on('error', () => resolve('error')) socket.on('timeout', () => resolve('timeout')) socket.on('close', () => { }) }) } // ============= 后厨-整单 ================ export function ALL_KITCHEN(printerIp, order) { // console.log('后厨-整单', JSON.stringify(order)) // 打印日志,检查数据结构; return new Promise(resolve => { const socket = new net.Socket() const lineWidth = 48 socket.setTimeout(8000) socket.connect(9100, printerIp, () => { try { setupPrinter(socket) const title1 = order.shop_name || ''; const title2 = '后厨总单'; // 核心修复:先写入空行清空打印机缓冲区,避免残留字符干扰居中起始位置 socket.write(iconv.encode('\n', 'gbk')); // 步骤1:重置打印机格式(避免残留格式影响) socket.write(Buffer.from([0x1B, 0x40])); // 步骤2:居中对齐标题 socket.write(Buffer.from([0x1B, 0x61, 0x01])); // 1 = center // 步骤3:设置标题1的格式(加粗+大号字体) socket.write(Buffer.from([0x1B, 0x21, 0x11])); // 字号放大(0x11 是常用放大值) socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗 socket.write(iconv.encode(title1 + '\n', 'gbk')); // 步骤4:恢复格式,打印标题2(常规字体) socket.write(Buffer.from([0x1B, 0x21, 0x00])); // 恢复默认字号 socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗 socket.write(iconv.encode(title2 + '\n\n', 'gbk')); socket.write(Buffer.from([0x1B, 0x61, 0x00])); // 0 = left socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗 let header1 = padLeftAlign('桌台号:', 6) + padRightAlign(order.orderInfo.tableName || '无') + '\n' socket.write(iconv.encode(header1, 'gbk')); socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗 const dineModeText = dineModeFilter(order.orderInfo.dineMode) let header2 = padLeftAlign(`${order.deviceName}-${dineModeText}`) + '\n' socket.write(iconv.encode(header2, 'gbk')); let tableInfo = '' tableInfo += createDivider(lineWidth, 'thin') + '\n' tableInfo += padLeftAlign('品名', 6) + padRightAlign('数量', 42) + '\n' order.carts.forEach(item => { tableInfo += padLeftAlign(item.name, 24) + padRightAlign(item.number, 24) + '\n' if (item.skuName.length > 0) { tableInfo += padLeftAlign(` 规格:${item.skuName}`, 48) + '\n' } if (item.remark.length > 0) { tableInfo += padLeftAlign(` 备注:${item.remark}`, 48) + '\n' } }) tableInfo += createDivider(lineWidth, 'thin') + '\n' tableInfo += padLeftAlign(`备注:${order.remark}`, 48) + '\n' tableInfo += createDivider(lineWidth, 'thin') + '\n' socket.write(iconv.encode(tableInfo, 'gbk')) // ======================== 底部 ======================== let bottom = '' bottom += padLeftAlign('操作员:', 8) + padLeftAlign(order.loginAccount || '无', 38) + '\n' bottom += padLeftAlign('打印时间:', 10) + padLeftAlign(dayjs(order.printTime).format(timeFormt), 38) + '\n' bottom += padLeftAlign('订单号:', 8) + padLeftAlign(order.orderInfo.orderNo || '-', 38) + '\n' bottom += createDivider(lineWidth, 'full') + '\n' bottom += '\n\n' bottom += '\n\n' socket.write(iconv.encode(bottom, 'gbk')) socket.write(Buffer.from([0x1D, 0x56, 0x00])) setTimeout(() => { socket.end() resolve('success') }, 400) } catch (err) { console.error(err) socket.destroy() resolve('error') } }) }) } // ============= 后厨-分单 一菜一品 ================ export function ONLY_KITCHEN(printerIp, order) { console.log('后厨-分单 一菜一品', JSON.stringify(order)) // 打印日志,检查数据结构; return new Promise(resolve => { const socket = new net.Socket(); socket.setTimeout(8000); socket.connect(9100, printerIp, () => { try { setupPrinter(socket); const title1 = order.orderInfo.tableName || ''; const title2 = `时间:${dayjs().format(timeFormt)}`; // 1. 重置打印机 socket.write(Buffer.from([0x1B, 0x40])); // 2. 居中对齐 socket.write(Buffer.from([0x1B, 0x61, 0x01])); // --- 关键修改区域 --- // 3. 设置字体为标准字体A (防止英文变窄) socket.write(Buffer.from([0x1B, 0x4D, 0x00])); // 4. 只开启加粗,不再放大 (去掉了 0x1B 0x21 0x10) socket.write(Buffer.from([0x1B, 0x45, 0x01])); // --- 关键修改区域 --- // 写入标题1 socket.write(Buffer.from([0x1D, 0x21, 0x11])); // 字体高宽双倍 socket.write(iconv.encode(title1 + '\n', 'gbk')); socket.write(Buffer.from([0x1D, 0x21, 0x00])); // 恢复默认字体 // 5. 恢复默认格式 (取消加粗) socket.write(Buffer.from([0x1B, 0x45, 0x00])); socket.write(iconv.encode(title2 + '\n\n', 'gbk')); socket.write(Buffer.from([0x1B, 0x61, 0x00])); // 左对齐 // --- 菜品内容部分 --- let tableInfo = ''; order.carts.forEach(item => { tableInfo += padLeftAlign(`${item.name} x ${item.number}`, 48) + '\n'; if (item.proGroupInfo) { item.proGroupInfo.forEach(subItem => { tableInfo += `>${subItem.proName} x ${subItem.number}` + '\n'; }); } if (item.skuName.length > 0) { tableInfo += padLeftAlign(`规格:${item.skuName}`, 48) + '\n'; } if (item.remark.length > 0) { tableInfo += padLeftAlign(`备注:${item.remark}`, 48) + '\n'; } }); // 增加底部空白 tableInfo += '\n\n\n\n\n\n\n\n'; socket.write(Buffer.from([0x1D, 0x21, 0x11])); // 字体高宽双倍 socket.write(iconv.encode(tableInfo, 'gbk')); socket.write(Buffer.from([0x1D, 0x21, 0x00])); // 恢复默认字体 // 切纸 socket.write(Buffer.from([0x1D, 0x56, 0x00])); // 关闭连接 setTimeout(() => { socket.end(); resolve('success'); }, 400); } catch (err) { console.error(err); socket.destroy(); resolve('error'); } }); }); } // ============= 后厨-退菜单 ================ export function REFUND_KITCHEN(printerIp, order) { return new Promise(resolve => { const socket = new net.Socket() const lineWidth = 48 socket.setTimeout(8000) socket.connect(9100, printerIp, () => { try { setupPrinter(socket) const title1 = order.shop_name || ''; const title2 = '后厨-退菜单'; // 核心修复:先写入空行清空打印机缓冲区,避免残留字符干扰居中起始位置 socket.write(iconv.encode('\n', 'gbk')); // 步骤1:重置打印机格式(避免残留格式影响) socket.write(Buffer.from([0x1B, 0x40])); // 步骤2:居中对齐标题 socket.write(Buffer.from([0x1B, 0x61, 0x01])); // 1 = center // 步骤3:设置标题1的格式(加粗+大号字体) socket.write(Buffer.from([0x1B, 0x21, 0x11])); // 字号放大(0x11 是常用放大值) socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗 socket.write(iconv.encode(title1 + '\n', 'gbk')); // 步骤4:恢复格式,打印标题2(常规字体) socket.write(Buffer.from([0x1B, 0x21, 0x00])); // 恢复默认字号 socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗 socket.write(iconv.encode(title2 + '\n\n', 'gbk')); socket.write(Buffer.from([0x1B, 0x61, 0x00])); // 0 = left socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗 let header1 = padLeftAlign('桌台号:', 6) + padRightAlign(order.orderInfo.tableName || '无') + '\n' socket.write(iconv.encode(header1, 'gbk')); socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗 const dineModeText = dineModeFilter(order.orderInfo.dineMode) let header2 = padLeftAlign(`${order.deviceName}-${dineModeText}`) + '\n' socket.write(iconv.encode(header2, 'gbk')); let tableInfo = '' tableInfo += createDivider(lineWidth, 'thin') + '\n' tableInfo += padLeftAlign('品名', 6) + padRightAlign('数量', 42) + '\n' order.carts.forEach(item => { tableInfo += padLeftAlign(item.name, 24) + padRightAlign(item.number, 24) + '\n' // if (item.skuName.length > 0) { // tableInfo += padLeftAlign(` 规格:${item.skuName}`, 48) + '\n' // } // if (item.remark.length > 0) { // tableInfo += padLeftAlign(` 备注:${item.remark}`, 48) + '\n' // } }) tableInfo += createDivider(lineWidth, 'thin') + '\n' // tableInfo += padLeftAlign(`备注:${order.remark}`, 48) + '\n' // tableInfo += createDivider(lineWidth, 'thin') + '\n' socket.write(iconv.encode(tableInfo, 'gbk')) // ======================== 底部 ======================== let bottom = '' bottom += padLeftAlign('操作员:', 8) + padLeftAlign(order.loginAccount || '无', 38) + '\n' bottom += padLeftAlign('打印时间:', 10) + padLeftAlign(dayjs(order.printTime).format(timeFormt), 38) + '\n' bottom += padLeftAlign('订单号:', 8) + padLeftAlign(order.orderInfo.orderNo || '-', 38) + '\n' bottom += createDivider(lineWidth, 'full') + '\n' bottom += '\n' bottom += '\n' bottom += '\n' bottom += '\n' bottom += '\n' bottom += '\n' socket.write(iconv.encode(bottom, 'gbk')) socket.write(Buffer.from([0x1D, 0x56, 0x00])) setTimeout(() => { socket.end() resolve('success') }, 400) } catch (err) { console.error(err) socket.destroy() resolve('error') } }) }) } // ======================== 退款小票 ======================== export function REFUND_ORDER(printerIp, order) { console.log(JSON.stringify(order)); return new Promise((resolve) => { const socket = new net.Socket() const lineWidth = 48 socket.setTimeout(8000) const formatAmount = (amount) => Number(amount || 0).toFixed(2) socket.connect(9100, printerIp, () => { try { setupPrinter(socket) // ======================== 标题区域(彻底修复居中问题!) ======================== const title1 = order.shop_name || ''; let title2 = '退款单'; // 核心修复:先写入空行清空打印机缓冲区,避免残留字符干扰居中起始位置 socket.write(iconv.encode('\n', 'gbk')); // 步骤1:重置打印机格式(避免残留格式影响) socket.write(Buffer.from([0x1B, 0x40])); // 步骤2:居中对齐标题 socket.write(Buffer.from([0x1B, 0x61, 0x01])); // 1 = center // 步骤3:设置标题1的格式(加粗+大号字体) socket.write(Buffer.from([0x1B, 0x21, 0x11])); // 字号放大(0x11 是常用放大值) socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗 socket.write(iconv.encode(title1 + '\n', 'gbk')); // 步骤4:恢复格式,打印标题2(常规字体) socket.write(Buffer.from([0x1B, 0x21, 0x00])); // 恢复默认字号 socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗 socket.write(iconv.encode(title2 + '\n\n', 'gbk')); socket.write(Buffer.from([0x1B, 0x61, 0x00])); // 0 = left // ======================== 订单信息(修复显示+换行问题) ======================== const tableLine = padLeftAlign('桌台号:', 8) + padLeftAlign(order.orderInfo.tableName || '无', 14) + '\n' socket.write(Buffer.from([0x1B, 0x21, 0x00])); // 正常字号 socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗 socket.write(iconv.encode(tableLine, 'gbk')); socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗 let orderInfo = '' // 修复:打印机-用餐模式 + 就餐人数 展示逻辑 const dineModeText = dineModeFilter(order.orderInfo.dineMode) // 1. 拼接打印机+用餐模式文本,空值兜底 const printerDineText = `${order.deviceName || '未知打印机'}-${dineModeText || '未知模式'}` // 2. 截断超长文本(左侧分配20宽度,保证不超限) const truncatedPrinterDine = truncateByPrintWidth(printerDineText, 20) // 3. 左侧左对齐填充20宽度(保证占满分配空间,不浪费空白) const leftPart = padLeftAlign(truncatedPrinterDine, 20) // 4. 右侧就餐人数:空值兜底为“无”,右对齐填充28宽度(20+28=48,刚好行宽) const seatNumText = order.orderInfo.seatNum || '0' const rightPart = padRightAlign(`${seatNumText}人`, 28) // 加标签更清晰,也可只显示数字 if (order.carts.length > 0) { orderInfo += leftPart + rightPart + '\n' orderInfo += createDivider(lineWidth, 'thin') + '\n' // ======================== 商品表格 ======================== orderInfo += padLeftAlign('品名', 18) orderInfo += padLeftAlign('单价', 8) orderInfo += padLeftAlign('数量', 6) orderInfo += padRightAlign('小计', 16) + '\n' orderInfo += createDivider(lineWidth, 'thin') + '\n' order.carts.forEach(item => { const name = truncateByPrintWidth(item.name || '未知商品', 18) const price = formatAmount(item.salePrice) const num = item.number || 1 const total = formatAmount(item.totalAmount) orderInfo += padLeftAlign(name, 18) orderInfo += padLeftAlign(price, 8) orderInfo += padLeftAlign(num, 6) orderInfo += padRightAlign(total, 16) + '\n' }) orderInfo += createDivider(lineWidth, 'thin') + '\n' socket.write(iconv.encode(orderInfo, 'gbk')) } // ======================== 付款信息 ======================== let payableLine = '' payableLine += padLeftAlign('退款总计', 10) + padRightAlign(formatAmount(order.amount), 51) + '\n' socket.write(Buffer.from([0x1B, 0x21, 0x11])); // 放大字号 socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗 socket.write(iconv.encode(payableLine, 'gbk')); socket.write(Buffer.from([0x1B, 0x21, 0x00])); // 恢复默认字号 socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗 const refundType = { payBack: '原路退回', cash: '现金退款', } let refundInfo = '' refundInfo += padLeftAlign('退款方式:', 10) + padLeftAlign(refundType[order.orderInfo.refundType] || '无', 38) + '\n' refundInfo += padLeftAlign('退款原因:', 10) + padLeftAlign(order.orderInfo.refundRemark || '无', 38) + '\n' socket.write(iconv.encode(refundInfo, 'gbk')); socket.write(iconv.encode(createDivider(lineWidth, 'thin') + '\n', 'gbk')); // ======================== 底部 ======================== let bottom = '' bottom += padLeftAlign('操作员:', 8) + padLeftAlign(order.loginAccount || '无', 38) + '\n' bottom += padLeftAlign('打印时间:', 10) + padLeftAlign(new Date().toLocaleString(), 38) + '\n' bottom += padLeftAlign('原订单号:', 10) + padLeftAlign(order.orderInfo.orderNo || '-', 38) + '\n' bottom += createDivider(lineWidth, 'full') + '\n' bottom += '\n\n' bottom += '\n\n' socket.write(iconv.encode(bottom, 'gbk')) socket.write(Buffer.from([0x1D, 0x56, 0x00])) setTimeout(() => { socket.end() resolve('success') }, 400) } catch (err) { console.error(err) socket.destroy() resolve('error') } }) socket.on('error', () => resolve('error')) socket.on('timeout', () => resolve('timeout')) socket.on('close', () => { }) }) } // ======================== 打印退菜单 ======================== export function RETURN_ORDER(printerIp, order) { console.log(JSON.stringify(order)); return new Promise((resolve) => { const socket = new net.Socket() const lineWidth = 48 socket.setTimeout(8000) const formatAmount = (amount) => Number(amount || 0).toFixed(2) socket.connect(9100, printerIp, () => { try { setupPrinter(socket) // ======================== 标题区域(彻底修复居中问题!) ======================== const title1 = order.shop_name || ''; let title2 = '退菜单' // 核心修复:先写入空行清空打印机缓冲区,避免残留字符干扰居中起始位置 socket.write(iconv.encode('\n', 'gbk')); // 步骤1:重置打印机格式(避免残留格式影响) socket.write(Buffer.from([0x1B, 0x40])); // 步骤2:居中对齐标题 socket.write(Buffer.from([0x1B, 0x61, 0x01])); // 1 = center // 步骤3:设置标题1的格式(加粗+大号字体) socket.write(Buffer.from([0x1B, 0x21, 0x11])); // 字号放大(0x11 是常用放大值) socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗 socket.write(iconv.encode(title1 + '\n', 'gbk')); // 步骤4:恢复格式,打印标题2(常规字体) socket.write(Buffer.from([0x1B, 0x21, 0x00])); // 恢复默认字号 socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗 socket.write(iconv.encode(title2 + '\n\n', 'gbk')); socket.write(Buffer.from([0x1B, 0x61, 0x00])); // 0 = left // ======================== 订单信息(修复显示+换行问题) ======================== const tableLine = padLeftAlign('桌台号:', 8) + padLeftAlign(order.orderInfo.tableName || '无', 14) + '\n' socket.write(Buffer.from([0x1B, 0x21, 0x00])); // 正常字号 socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗 socket.write(iconv.encode(tableLine, 'gbk')); socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗 let orderInfo = '' // 修复:打印机-用餐模式 + 就餐人数 展示逻辑 const dineModeText = dineModeFilter(order.orderInfo.dineMode) // 1. 拼接打印机+用餐模式文本,空值兜底 const printerDineText = `${order.deviceName || '未知打印机'}-${dineModeText || '未知模式'}` // 2. 截断超长文本(左侧分配20宽度,保证不超限) const truncatedPrinterDine = truncateByPrintWidth(printerDineText, 20) // 3. 左侧左对齐填充20宽度(保证占满分配空间,不浪费空白) const leftPart = padLeftAlign(truncatedPrinterDine, 20) // 4. 右侧就餐人数:空值兜底为“无”,右对齐填充28宽度(20+28=48,刚好行宽) const seatNumText = order.orderInfo.seatNum || '0' const rightPart = padRightAlign(`${seatNumText}人`, 28) // 加标签更清晰,也可只显示数字 if (order.carts.length > 0) { orderInfo += leftPart + rightPart + '\n' orderInfo += createDivider(lineWidth, 'thin') + '\n' // ======================== 商品表格 ======================== orderInfo += padLeftAlign('品名', 18) orderInfo += padLeftAlign('单价', 8) orderInfo += padLeftAlign('数量', 6) orderInfo += padRightAlign('小计', 16) + '\n' orderInfo += createDivider(lineWidth, 'thin') + '\n' order.carts.forEach(item => { const name = truncateByPrintWidth(item.name || '未知商品', 18) const price = formatAmount(item.salePrice) const num = item.number || 1 const total = formatAmount(item.totalAmount) orderInfo += padLeftAlign(name, 18) orderInfo += padLeftAlign(price, 8) orderInfo += padLeftAlign(num, 6) orderInfo += padRightAlign(total, 16) + '\n' }) orderInfo += createDivider(lineWidth, 'thin') + '\n' socket.write(iconv.encode(orderInfo, 'gbk')) } // ======================== 付款信息 ======================== // if (order.isGuest) { // let payableLine = '' // payableLine += padLeftAlign('退款总计', 10) + padRightAlign(formatAmount(order.amount), 51) + '\n' // socket.write(Buffer.from([0x1B, 0x21, 0x11])); // 放大字号 // socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗 // socket.write(iconv.encode(payableLine, 'gbk')); // socket.write(Buffer.from([0x1B, 0x21, 0x00])); // 恢复默认字号 // socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗 // } // let refundInfo = '' // refundInfo += padLeftAlign('退款方式:', 10) + padLeftAlign(order.refundMethod || '无', 38) + '\n' // refundInfo += padLeftAlign('退款原因:', 10) + padLeftAlign(order.remark || '无', 38) + '\n' // socket.write(iconv.encode(refundInfo, 'gbk')); // socket.write(iconv.encode(createDivider(lineWidth, 'thin') + '\n', 'gbk')); // ======================== 底部 ======================== let bottom = '' bottom += padLeftAlign('操作员:', 8) + padLeftAlign(order.loginAccount || '无', 38) + '\n' bottom += padLeftAlign('打印时间:', 10) + padLeftAlign(new Date().toLocaleString(), 38) + '\n' bottom += padLeftAlign('原订单号:', 10) + padLeftAlign(order.orderInfo.orderNo || '-', 38) + '\n' bottom += createDivider(lineWidth, 'full') + '\n' bottom += '\n\n' bottom += '\n\n' socket.write(iconv.encode(bottom, 'gbk')) socket.write(Buffer.from([0x1D, 0x56, 0x00])) setTimeout(() => { socket.end() resolve('success') }, 400) } catch (err) { console.error(err) socket.destroy() resolve('error') } }) socket.on('error', () => resolve('error')) socket.on('timeout', () => resolve('timeout')) socket.on('close', () => { }) }) } // ======================== 打印交班小票(完整版 · 补齐分类/商品数据) ======================== export function printHandoverReceipt(printerIp, data) { return new Promise((resolve) => { const socket = new net.Socket() const lineWidth = 48 socket.setTimeout(8000) socket.connect(9100, printerIp, () => { try { setupPrinter(socket) // ======================== 标题区域(统一结算小票的居中修复逻辑) ======================== const title1 = data.shopName || '店铺'; const title2 = '交班小票'; // 核心修复:先写入空行清空打印机缓冲区,避免残留字符干扰居中起始位置 socket.write(iconv.encode('\n', 'gbk')); // 步骤1:重置打印机格式(避免残留格式影响) socket.write(Buffer.from([0x1B, 0x40])); // 步骤2:居中对齐标题 socket.write(Buffer.from([0x1B, 0x61, 0x01])); // 1 = center // 步骤3:设置标题1的格式(加粗+大号字体) socket.write(Buffer.from([0x1B, 0x21, 0x11])); // 字号放大(0x11 是常用放大值) socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗 socket.write(iconv.encode(title1 + '\n', 'gbk')); // 步骤4:恢复格式,打印标题2(常规字体) socket.write(Buffer.from([0x1B, 0x21, 0x00])); // 恢复默认字号 socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗 socket.write(iconv.encode(title2 + '\n\n', 'gbk')); socket.write(Buffer.from([0x1B, 0x61, 0x00])); // 0 = left socket.write(iconv.encode(createDivider(lineWidth, 'thin') + '\n', 'gbk')); // ======================== 基础信息区域(拆分当班/交班时间) ======================== let basicInfo = '' // 优化:拆分当班时间和交班时间,空值兜底 basicInfo += padLeftAlign('交班时间:', 10) + padLeftAlign(data.handoverTime || '-', 36) + '\n' basicInfo += padLeftAlign('交班人:', 8) + padLeftAlign(data.staffName || '-', 36) + '\n' basicInfo += padLeftAlign('交班:', 6) + padLeftAlign(`${data.loginTime} - ${data.handoverTime}` || '-', 36) + '\n\n' // basicInfo += createDivider(lineWidth, 'thin') + '\n' // 收入明细 - 补充会员支付/充值字段 basicInfo += padLeftAlign('当班营业总额:', 10) + padLeftAlign(formatAmount(data.turnover), 36) + '\n' basicInfo += padLeftAlign('实际收款的支付方式', 10) + '\n' basicInfo += padLeftAlign('现金', 10) + padRightAlign(formatAmount(data.cash), 36) + '\n' basicInfo += padLeftAlign('微信', 10) + padRightAlign(formatAmount(data.wechat), 36) + '\n' basicInfo += padLeftAlign('支付宝', 10) + padRightAlign(formatAmount(data.alipay), 36) + '\n' basicInfo += padLeftAlign('二维码收款', 10) + padRightAlign(formatAmount(data.selfScan), 36) + '\n' basicInfo += padLeftAlign('扫码收款', 10) + padRightAlign(formatAmount(data.barScan), 36) + '\n' basicInfo += padLeftAlign('充值', 10) + padRightAlign(formatAmount(data.recharge), 36) + '\n\n' basicInfo += padLeftAlign('非实际收款的支付方式', 10) + '\n' basicInfo += padLeftAlign('挂账', 10) + padRightAlign(formatAmount(data.owed), 36) + '\n' basicInfo += padLeftAlign('余额', 10) + padRightAlign(formatAmount(data.balance), 36) + '\n' basicInfo += createDivider(lineWidth, 'thin') + '\n' socket.write(Buffer.from([0x1B, 0x21, 0x00])); // 正常字号 socket.write(iconv.encode(basicInfo, 'gbk')) let refundInfo = '' refundInfo += padLeftAlign('退款/退菜', 10) + '\n' refundInfo += padLeftAlign('退款金额', 10) + padRightAlign(formatAmount(data.refundAmount), 36) + '\n' refundInfo += padLeftAlign('退菜数量', 10) + padRightAlign(formatAmount(data.returnDishCount), 36) + '\n' refundInfo += createDivider(lineWidth, 'thin') + '\n' socket.write(iconv.encode(refundInfo, 'gbk')) let orderInfo = '' const orderLabel = '订单(数量/订单总额)' const orderValue = `${data.orderCount}/${formatAmount(data.orderTurnover)}` const orderLabelWidth = lineWidth - getPrintWidth(orderValue) orderInfo += padLeftAlign(orderLabel, orderLabelWidth) + orderValue + '\n\n' socket.write(iconv.encode(orderInfo, 'gbk')) // ======================== 分类数据区域 ======================== let categoryInfo = '' if (data.categoryList && data.categoryList.length) { categoryInfo += centerAlign('销售数据', lineWidth) + '\n' categoryInfo += createDivider(lineWidth, 'thin') + '\n' // 分类表头 categoryInfo += padLeftAlign('商品分类', 24) + padLeftAlign('数量', 12) + padRightAlign('总计', 12) + '\n' // categoryInfo += createDivider(lineWidth, 'thin') + '\n' // 分类数据行 data.categoryList.forEach(item => { const catName = truncateByPrintWidth(item.categoryName || '未知分类', 24) const num = item.num || 0 const amount = formatAmount(item.amount) categoryInfo += padLeftAlign(catName, 24) + padLeftAlign(num, 12) + padRightAlign(amount, 12) + '\n' }) categoryInfo += createDivider(lineWidth, 'thin') + '\n' } socket.write(iconv.encode(categoryInfo, 'gbk')) // ======================== 商品数据区域 ======================== // let productInfo = '' // if (data.detailList && data.detailList.length) { // productInfo += centerAlign('商品数据', lineWidth) + '\n' // productInfo += createDivider(lineWidth, 'thin') + '\n' // // 商品表头 // productInfo += padLeftAlign('商品', 36) + padRightAlign('数量', 12) + '\n' // // productInfo += createDivider(lineWidth, 'thin') + '\n' // // 商品数据行 // data.detailList.forEach(item => { // const prodName = truncateByPrintWidth(item.productName || '未知商品', 36) // const num = item.num || 0 // productInfo += padLeftAlign(prodName, 36) + padRightAlign(num, 12) + '\n' // }) // productInfo += createDivider(lineWidth, 'thin') + '\n' // } // socket.write(iconv.encode(productInfo, 'gbk')) // ======================== 底部区域(统一结算小票风格) ======================== let bottomInfo = '' bottomInfo += padLeftAlign('打印时间:', 12) + padRightAlign(data.printTime || new Date().toLocaleString(), 36) + '\n' bottomInfo += createDivider(lineWidth, 'full') + '\n' // 增加空行,避免打印内容紧贴纸边 bottomInfo += '\n\n' bottomInfo += '\n\n' socket.write(iconv.encode(bottomInfo, 'gbk')) // 切纸指令(统一结算小票的切纸逻辑) socket.write(Buffer.from([0x1D, 0x56, 0x00])) // 统一延迟关闭连接,保证数据完整发送 setTimeout(() => { socket.end() resolve('success') }, 400) } catch (err) { console.error('打印交班小票异常:', err) socket.destroy() resolve('error') } }) // 统一错误处理(对齐结算小票的错误处理逻辑) socket.on('error', (err) => { console.error('打印机连接错误:', err) resolve('error') }) socket.on('timeout', () => resolve('timeout')) socket.on('close', () => { }) }) } // ================ 打印盘点单 ================= export function STOCK_CHECK(printerIp, data) { return new Promise(resolve => { console.log(data); const socket = new net.Socket() const lineWidth = 48 socket.setTimeout(8000) socket.connect(9100, printerIp, () => { try { setupPrinter(socket) // ============== 标题区域 ================ const title1 = data.shop_name || '' const title2 = '盘点单' // 核心修复:先写入空行清空打印机缓冲区,避免残留字符干扰居中起始位置 socket.write(iconv.encode('\n', 'gbk')); // 步骤1:重置打印机格式(避免残留格式影响) socket.write(Buffer.from([0x1B, 0x40])); // 步骤2:居中对齐标题 socket.write(Buffer.from([0x1B, 0x61, 0x01])); // 1 = center // 步骤3:设置标题1的格式(加粗+大号字体) socket.write(Buffer.from([0x1B, 0x21, 0x11])); // 字号放大(0x11 是常用放大值) socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗 socket.write(iconv.encode(title1 + '\n', 'gbk')); // 步骤4:恢复格式,打印标题2(常规字体) socket.write(Buffer.from([0x1B, 0x21, 0x00])); // 恢复默认字号 socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗 socket.write(iconv.encode(title2 + '\n\n', 'gbk')); socket.write(Buffer.from([0x1B, 0x61, 0x00])); // 0 = left // ============== 盘点信息区域 ================ let tableInfo = '' // tableInfo += createDivider(lineWidth, 'thin') + '\n' tableInfo += padLeftAlign('耗材名称', 18) tableInfo += padLeftAlign('单价', 8) tableInfo += padLeftAlign('实际数', 6) tableInfo += padRightAlign('盈亏数', 16) + '\n' // tableInfo += createDivider(lineWidth, 'thin') + '\n' data.items.forEach(item => { const name = truncateByPrintWidth(item.consName || '未知商品', 18) const price = `${item.price}/${item.unit}` const num = item.actualNumber const total = item.winLossNumber tableInfo += padLeftAlign(name, 18) tableInfo += padLeftAlign(price, 8) tableInfo += padLeftAlign(num, 6) tableInfo += padRightAlign(total, 16) + '\n' }) tableInfo += createDivider(lineWidth, 'thin') + '\n' socket.write(iconv.encode(tableInfo, 'gbk')) // ============== 盘点统计 ================ let totalInfo = '' totalInfo += padLeftAlign('账存数量:', 10) + padLeftAlign(data.winLossNumberCount, 38) + '\n' totalInfo += padLeftAlign('盈亏金额:', 10) + padLeftAlign(data.winLossAmount, 38) + '\n' totalInfo += padLeftAlign('备注:', 6) + padLeftAlign(data.remark, 38) + '\n' socket.write(iconv.encode(totalInfo, 'gbk')); socket.write(iconv.encode(createDivider(lineWidth, 'thin') + '\n', 'gbk')); // ============== 尾部信息 ================ let bottom = '' bottom += padLeftAlign('操作员:', 8) + padLeftAlign(data.operator || '无', 38) + '\n' bottom += padLeftAlign('打印时间:', 10) + padLeftAlign(dayjs().format('YYYY/M/D HH:mm:ss'), 38) + '\n' bottom += createDivider(lineWidth, 'full') + '\n' bottom += '\n\n' bottom += '\n\n' socket.write(iconv.encode(bottom, 'gbk')) socket.write(Buffer.from([0x1D, 0x56, 0x00])) setTimeout(() => { socket.end() resolve('success') }, 400) } catch (err) { console.error(err) socket.destroy() resolve('error') } }) }) } // ================ 商品报表 ================= export function PRODUCT_REPORT(printerIp, data) { return new Promise(resolve => { console.log('商品报表', JSON.stringify(data)); const socket = new net.Socket() const lineWidth = 48 socket.setTimeout(8000) socket.connect(9100, printerIp, () => { try { setupPrinter(socket) // ============== 标题区域 ================ const title1 = data.shop_name || '' const title2 = '商品报表' // 核心修复:先写入空行清空打印机缓冲区,避免残留字符干扰居中起始位置 socket.write(iconv.encode('\n', 'gbk')); // 步骤1:重置打印机格式(避免残留格式影响) socket.write(Buffer.from([0x1B, 0x40])); // 步骤2:居中对齐标题 socket.write(Buffer.from([0x1B, 0x61, 0x01])); // 1 = center // 步骤3:设置标题1的格式(加粗+大号字体) socket.write(Buffer.from([0x1B, 0x21, 0x11])); // 字号放大(0x11 是常用放大值) socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗 socket.write(iconv.encode(title1 + '\n', 'gbk')); // 步骤4:恢复格式,打印标题2(常规字体) socket.write(Buffer.from([0x1B, 0x21, 0x00])); // 恢复默认字号 socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗 socket.write(iconv.encode(title2 + '\n\n', 'gbk')); socket.write(Buffer.from([0x1B, 0x61, 0x00])); // 0 = left // 打印信息区域 let infoData = '' infoData += padLeftAlign('打印时间:', 10) + padLeftAlign(dayjs().format('YYYY/M/D HH:mm:ss'), 38) + '\n' infoData += padLeftAlign('操作人:', 8) + padLeftAlign(data.data.operator, 38) + '\n' infoData += padLeftAlign('统计时间:', 10) + padLeftAlign(data.data.statisticsTime, 38) + '\n\n' infoData += padLeftAlign(`总计商品${data.data.totalProductCount}件,实收金额${data.data.totalActualAmount}元`, 38) + '\n' infoData += createDivider(lineWidth, 'thin') + '\n' socket.write(iconv.encode(infoData, 'gbk')) // ============== 盘点信息区域 ================ let tableInfo = '' tableInfo += padLeftAlign('商品', 18) tableInfo += padLeftAlign('数量', 8) tableInfo += padLeftAlign('实收', 6) tableInfo += padRightAlign('销售额', 16) + '\n' if (data.data.items && data.data.items.length > 0) { data.data.items.forEach(item => { // 套餐主项:正常显示 名称、单价、数量、小计 const packageName = truncateByPrintWidth(item.categoryName, 18) tableInfo += padLeftAlign(packageName, 18) tableInfo += padLeftAlign(item.number, 8) tableInfo += padLeftAlign(item.actualAmount || 1, 6) tableInfo += padRightAlign(item.salesAmount, 16) + '\n' // 套餐子项:只显示名称+规格,单价、小计全部为空 const proGroupInfo = item.productItems || [] proGroupInfo.forEach(subItem => { const subName = truncateByPrintWidth(` ${subItem.productName}`, 18) tableInfo += padLeftAlign(subName, 18) tableInfo += padLeftAlign(subItem.number, 8) // 子项单价空 tableInfo += padLeftAlign(subItem.actualAmount || 1, 6) tableInfo += padRightAlign(subItem.salesAmount, 16) + '\n' // 子项小计空 }) }) } socket.write(iconv.encode(tableInfo, 'gbk')) // // ============== 尾部信息 ================ let bottom = '' bottom += createDivider(lineWidth, 'full') + '\n' bottom += '\n\n' bottom += '\n\n' socket.write(iconv.encode(bottom, 'gbk')) socket.write(Buffer.from([0x1D, 0x56, 0x00])) setTimeout(() => { socket.end() resolve('success') }, 400) } catch (err) { console.error(err) socket.destroy() resolve('error') } }) }) } // ========== 经营日报 ========== export function DAY_REPORT(printerIp, data) { return new Promise(resolve => { console.log('经营日报', data); const socket = new net.Socket() const lineWidth = 48 socket.setTimeout(8000) socket.connect(9100, printerIp, () => { try { setupPrinter(socket) // ============== 标题区域 ================ const title1 = data.shop_name || '' const title2 = '经营日报' // 核心修复:先写入空行清空打印机缓冲区,避免残留字符干扰居中起始位置 socket.write(iconv.encode('\n', 'gbk')); // 步骤1:重置打印机格式(避免残留格式影响) socket.write(Buffer.from([0x1B, 0x40])); // 步骤2:居中对齐标题 socket.write(Buffer.from([0x1B, 0x61, 0x01])); // 1 = center // 步骤3:设置标题1的格式(加粗+大号字体) socket.write(Buffer.from([0x1B, 0x21, 0x11])); // 字号放大(0x11 是常用放大值) socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗 socket.write(iconv.encode(title1 + '\n', 'gbk')); // 步骤4:恢复格式,打印标题2(常规字体) socket.write(Buffer.from([0x1B, 0x21, 0x00])); // 恢复默认字号 socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗 socket.write(iconv.encode(title2 + '\n\n', 'gbk')); socket.write(Buffer.from([0x1B, 0x61, 0x00])); // 0 = left // 打印信息区域 let infoData = '' infoData += padLeftAlign('打印时间:', 10) + padLeftAlign(dayjs().format('YYYY/MM/DD HH:mm:ss'), 38) + '\n' infoData += padLeftAlign('操作人:', 8) + padLeftAlign(data.data.operator, 38) + '\n' infoData += padLeftAlign('统计时间:', 10) + padLeftAlign(dayjs(data.data.statisticsTime).format('YYYY/MM/DD'), 38) + '\n' infoData += createDivider(lineWidth, 'thin') + '\n' socket.write(iconv.encode(infoData, 'gbk')) // 营业指标 socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗 const line1 = padLeftAlign('营业指标', 10) + '\n'; socket.write(iconv.encode(line1, 'gbk')); socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗 let businessInfo = '' businessInfo += padLeftAlign(' 订单原价总额', 14) + padRightAlign(formatAmount(data.data.turnover.originAmount), 34) + '\n' businessInfo += padLeftAlign(' 营业额', 8) + padRightAlign(formatAmount(data.data.turnover.turnover), 40) + '\n' businessInfo += padLeftAlign(' 优惠金额', 10) + padRightAlign(formatAmount(data.data.turnover.discountAmount), 38) + '\n' businessInfo += padLeftAlign(' 订单数(含退款)', 18) + padRightAlign(formatAmount(data.data.turnover.orderCount), 30) + '\n' businessInfo += padLeftAlign(' 折前单均价', 12) + padRightAlign(formatAmount(data.data.turnover.averageOrderAmount), 36) + '\n' businessInfo += padLeftAlign(' 折后单均价', 12) + padRightAlign(formatAmount(data.data.turnover.averageTurnover), 36) + '\n' businessInfo += padLeftAlign(' 退款金额', 10) + padRightAlign(formatAmount(data.data.turnover.refundAmount), 38) + '\n' businessInfo += padLeftAlign(' 退款订单数', 12) + padRightAlign(formatAmount(data.data.turnover.refundOrderCount), 36) + '\n' businessInfo += createDivider(lineWidth, 'thin') + '\n' socket.write(iconv.encode(businessInfo, 'gbk')) // 收入来源 socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗 const line2 = padLeftAlign('收入来源', 10) + '\n'; socket.write(iconv.encode(line2, 'gbk')); socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗 let info2 = '' info2 += padLeftAlign(' 现金', 6) + padRightAlign(formatAmount(data.data.sourceIncome.cash), 42) + '\n' info2 += padLeftAlign(' 微信', 6) + padRightAlign(formatAmount(data.data.sourceIncome.wechat), 42) + '\n' info2 += padLeftAlign(' 支付宝', 8) + padRightAlign(formatAmount(data.data.sourceIncome.alipay), 40) + '\n' info2 += padLeftAlign(' 美团团购', 12) + padRightAlign(formatAmount(data.data.sourceIncome.meituan), 36) + '\n' info2 += padLeftAlign(' 抖音团购', 12) + padRightAlign(formatAmount(data.data.sourceIncome.douyin), 36) + '\n' info2 += padLeftAlign(' 其它', 6) + padRightAlign(formatAmount(data.data.sourceIncome.other), 42) + '\n' info2 += createDivider(lineWidth, 'thin') + '\n' socket.write(iconv.encode(info2, 'gbk')) // 实收统计 socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗 const line3 = padLeftAlign('实收统计', 10) + '\n'; socket.write(iconv.encode(line3, 'gbk')); socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗 let info3 = '' info3 += padLeftAlign(' 现金', 6) + padRightAlign(formatAmount(data.data.actualIncome.cash), 42) + '\n' info3 += padLeftAlign(' 微信', 6) + padRightAlign(formatAmount(data.data.actualIncome.wechat), 42) + '\n' info3 += padLeftAlign(' 支付宝', 8) + padRightAlign(formatAmount(data.data.actualIncome.alipay), 40) + '\n' info3 += padLeftAlign(' 美团团购', 12) + padRightAlign(formatAmount(data.data.actualIncome.meituan), 36) + '\n' info3 += padLeftAlign(' 抖音团购', 12) + padRightAlign(formatAmount(data.data.actualIncome.douyin), 36) + '\n' info3 += padLeftAlign(' 其它', 6) + padRightAlign(formatAmount(data.data.actualIncome.other), 42) + '\n' info3 += createDivider(lineWidth, 'thin') + '\n' socket.write(iconv.encode(info3, 'gbk')) // 优惠统计 socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗 const line4 = padLeftAlign('优惠统计', 10) + '\n'; socket.write(iconv.encode(line4, 'gbk')); socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗 let info4 = '' info4 += padLeftAlign(' 新客立减', 12) + padRightAlign(formatAmount(data.data.discountSta.newConsumerDiscount), 36) + '\n' info4 += padLeftAlign(' 霸王餐', 10) + padRightAlign(formatAmount(data.data.discountSta.freeCashAmount), 38) + '\n' info4 += padLeftAlign(' 满减活动', 12) + padRightAlign(formatAmount(data.data.discountSta.fullMinusAmount), 36) + '\n' info4 += padLeftAlign(' 优惠券', 10) + padRightAlign(formatAmount(data.data.discountSta.couponAmount), 38) + '\n' info4 += padLeftAlign(' 会员折扣', 12) + padRightAlign(formatAmount(data.data.discountSta.memberDiscount), 36) + '\n' info4 += padLeftAlign(' 积分抵扣金额', 14) + padRightAlign(formatAmount(data.data.discountSta.pointsDiscountAmount), 34) + '\n' info4 += padLeftAlign(' 订单改价', 12) + padRightAlign(formatAmount(data.data.discountSta.orderDiscount), 36) + '\n' info4 += createDivider(lineWidth, 'thin') + '\n' socket.write(iconv.encode(info4, 'gbk')) // ============== 尾部信息 ================ let bottom = '' bottom += '\n\n' bottom += '\n\n' bottom += '\n\n' socket.write(iconv.encode(bottom, 'gbk')) socket.write(Buffer.from([0x1D, 0x56, 0x00])) setTimeout(() => { socket.end() resolve('success') }, 400) } catch (err) { console.error(err) socket.destroy() resolve('error') } }) }) } // ========== 日结单 ========== export function DAY_ORDER(printerIp, data) { return new Promise(resolve => { console.log('日结单', JSON.stringify(data)); const socket = new net.Socket() const lineWidth = 48 socket.setTimeout(8000) socket.connect(9100, printerIp, () => { try { setupPrinter(socket) // ============== 标题区域 ================ const title1 = data.shop_name || '' const title2 = '日结单' // 核心修复:先写入空行清空打印机缓冲区,避免残留字符干扰居中起始位置 socket.write(iconv.encode('\n', 'gbk')); // 步骤1:重置打印机格式(避免残留格式影响) socket.write(Buffer.from([0x1B, 0x40])); // 步骤2:居中对齐标题 socket.write(Buffer.from([0x1B, 0x61, 0x01])); // 1 = center // 步骤3:设置标题1的格式(加粗+大号字体) socket.write(Buffer.from([0x1B, 0x21, 0x11])); // 字号放大(0x11 是常用放大值) socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗 socket.write(iconv.encode(title1 + '\n', 'gbk')); // 步骤4:恢复格式,打印标题2(常规字体) socket.write(Buffer.from([0x1B, 0x21, 0x00])); // 恢复默认字号 socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗 socket.write(iconv.encode(title2 + '\n\n', 'gbk')); socket.write(Buffer.from([0x1B, 0x61, 0x00])); // 0 = left // 打印信息区域 let infoData = '' infoData += padLeftAlign('打印时间:', 10) + padLeftAlign(dayjs().format('YYYY/MM/DD HH:mm:ss'), 38) + '\n' infoData += padLeftAlign('操作人:', 8) + padLeftAlign(data.data.operator, 38) + '\n' infoData += padLeftAlign('统计时间:', 10) + padLeftAlign(dayjs(data.data.statisticsTime).format('YYYY/MM/DD'), 38) + '\n' infoData += createDivider(lineWidth, 'thin') + '\n' socket.write(iconv.encode(infoData, 'gbk')) // 营业指标 socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗 const line1 = padLeftAlign('营业统计', 10) + '\n'; socket.write(iconv.encode(line1, 'gbk')); socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗 let businessInfo = '' businessInfo += padLeftAlign(' 订单原价总额', 14) + padRightAlign(formatAmount(data.data.turnover.originAmount), 34) + '\n' businessInfo += padLeftAlign(' 营业额', 8) + padRightAlign(formatAmount(data.data.turnover.turnover), 40) + '\n' businessInfo += padLeftAlign(' 优惠金额', 10) + padRightAlign(formatAmount(data.data.turnover.discountAmount), 38) + '\n' businessInfo += padLeftAlign(' 订单数(含退款)', 18) + padRightAlign(formatAmount(data.data.turnover.orderCount), 30) + '\n' businessInfo += padLeftAlign(' 退款订单数', 12) + padRightAlign(formatAmount(data.data.turnover.refundOrderCount), 36) + '\n' businessInfo += padLeftAlign(' 退款金额', 12) + padRightAlign(formatAmount(data.data.turnover.refundAmount), 36) + '\n' businessInfo += padLeftAlign(' 现金收款', 10) + padRightAlign(formatAmount(data.data.turnover.cash), 38) + '\n' businessInfo += padLeftAlign(' 备用金', 12) + padRightAlign(formatAmount(data.data.turnover.reserve), 36) + '\n' businessInfo += padLeftAlign(' 钱箱剩余', 12) + padRightAlign(formatAmount(data.data.turnover.cashBoxRemaining), 36) + '\n' businessInfo += createDivider(lineWidth, 'thin') + '\n' socket.write(iconv.encode(businessInfo, 'gbk')) // 收款构成 socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗 const line2 = padLeftAlign('收款构成', 10) + '\n'; socket.write(iconv.encode(line2, 'gbk')); socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗 let info2 = '' info2 += padLeftAlign(' 现金', 6) + padRightAlign(formatAmount(data.data.income.cash), 42) + '\n' info2 += padLeftAlign(' 微信', 6) + padRightAlign(formatAmount(data.data.income.wechat), 42) + '\n' info2 += padLeftAlign(' 支付宝', 8) + padRightAlign(formatAmount(data.data.income.alipay), 40) + '\n' info2 += padLeftAlign(' 团购', 6) + padRightAlign(formatAmount(data.data.income.meituan), 42) + '\n' info2 += createDivider(lineWidth, 'thin') + '\n' socket.write(iconv.encode(info2, 'gbk')) // 优惠统计 socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗 const line4 = padLeftAlign('优惠统计', 10) + '\n'; socket.write(iconv.encode(line4, 'gbk')); socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗 let info4 = '' info4 += padLeftAlign(' 新客立减', 12) + padRightAlign(formatAmount(data.data.discountSta.newConsumerDiscount), 36) + '\n' info4 += padLeftAlign(' 霸王餐', 10) + padRightAlign(formatAmount(data.data.discountSta.freeCashAmount), 38) + '\n' info4 += padLeftAlign(' 满减活动', 12) + padRightAlign(formatAmount(data.data.discountSta.fullMinusAmount), 36) + '\n' info4 += padLeftAlign(' 优惠券', 10) + padRightAlign(formatAmount(data.data.discountSta.couponAmount), 38) + '\n' info4 += padLeftAlign(' 会员折扣', 12) + padRightAlign(formatAmount(data.data.discountSta.memberDiscount), 36) + '\n' info4 += padLeftAlign(' 积分抵扣金额', 14) + padRightAlign(formatAmount(data.data.discountSta.pointsDiscountAmount), 34) + '\n' info4 += padLeftAlign(' 订单改价', 12) + padRightAlign(formatAmount(data.data.discountSta.orderDiscount), 36) + '\n' info4 += createDivider(lineWidth, 'thin') + '\n' socket.write(iconv.encode(info4, 'gbk')) // 敏感操作记录 socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗 const line5 = padLeftAlign('敏感操作记录', 10) + '\n'; socket.write(iconv.encode(line5, 'gbk')); socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗 let info5 = '' data.data.operationRecords.forEach(item => { info5 += padLeftAlign(item.operation, 16) + padLeftAlign(`数量:${item.count}`, 16) + padRightAlign(`金额:${formatAmount(item.amount)}`, 16) + '\n' }) socket.write(iconv.encode(info5, 'gbk')) // ============== 尾部信息 ================ let bottom = '' bottom += '\n\n' bottom += '\n\n' bottom += '\n\n' socket.write(iconv.encode(bottom, 'gbk')) socket.write(Buffer.from([0x1D, 0x56, 0x00])) setTimeout(() => { socket.end() resolve('success') }, 400) } catch (err) { console.error(err) socket.destroy() resolve('error') } }) }) } // ========== 储值单 ========== export function RECHARGE(printerIp, data) { return new Promise(resolve => { console.log('储值单', data); const socket = new net.Socket() const lineWidth = 48 socket.setTimeout(8000) socket.connect(9100, printerIp, () => { try { setupPrinter(socket) // ============== 标题区域 ================ const title1 = data.shop_name || '' const title2 = '储值单' // 核心修复:先写入空行清空打印机缓冲区,避免残留字符干扰居中起始位置 socket.write(iconv.encode('\n', 'gbk')); // 步骤1:重置打印机格式(避免残留格式影响) socket.write(Buffer.from([0x1B, 0x40])); // 步骤2:居中对齐标题 socket.write(Buffer.from([0x1B, 0x61, 0x01])); // 1 = center // 步骤3:设置标题1的格式(加粗+大号字体) socket.write(Buffer.from([0x1B, 0x21, 0x11])); // 字号放大(0x11 是常用放大值) socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗 socket.write(iconv.encode(title1 + '\n', 'gbk')); // 步骤4:恢复格式,打印标题2(常规字体) socket.write(Buffer.from([0x1B, 0x21, 0x00])); // 恢复默认字号 socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗 socket.write(iconv.encode(title2 + '\n\n', 'gbk')); socket.write(Buffer.from([0x1B, 0x61, 0x00])); // 0 = left // 打印信息区域 let infoData = '' infoData += padLeftAlign('充值用户:', 10) + padLeftAlign(`${data.userName}`, 38) + '\n' infoData += padLeftAlign('手机号:', 8) + padLeftAlign(data.userPhone, 38) + '\n' infoData += padLeftAlign('支付时间:', 10) + padLeftAlign(dayjs(data.payTime).format('YYYY/MM/DD HH:mm:ss'), 38) + '\n' infoData += createDivider(lineWidth, 'thin') + '\n' socket.write(iconv.encode(infoData, 'gbk')) // ============== 盘点信息区域 ================ // ------------------------------ // 【充值金额 - 加粗】 // ------------------------------ socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗 const line1 = padLeftAlign('充值金额', 10) + padRightAlign(data.rechargeAmount, 38) + '\n'; socket.write(iconv.encode(line1, 'gbk')); socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗 // 赠送金额 if (data.giftAmount > 0) { const line2 = padLeftAlign('赠送金额', 10) + padRightAlign(data.giftAmount, 38) + '\n'; socket.write(iconv.encode(line2, 'gbk')); } // 赠送积分 if (data.giftPoints > 0) { const line3 = padLeftAlign('赠送积分', 10) + padRightAlign(data.giftPoints, 38) + '\n'; socket.write(iconv.encode(line3, 'gbk')); } // 赠送优惠券 if (data.giftCoupon > 0) { const line4 = padLeftAlign('赠送优惠券(张)', 13) + padRightAlign(data.giftCoupon, 30) + '\n'; socket.write(iconv.encode(line4, 'gbk')); } // 账户余额 const line5 = padLeftAlign('账户余额(充值后)', 13) + padRightAlign(data.balance, 30) + '\n'; socket.write(iconv.encode(line5, 'gbk')); // 分割线 const line6 = createDivider(lineWidth, 'thin') + '\n'; socket.write(iconv.encode(line6, 'gbk')); // ------------------------------ // 【已付金额 - 加粗】 // ------------------------------ socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗 const line7 = padLeftAlign('已付金额', 10) + padRightAlign(data.rechargeAmount, 38) + '\n'; socket.write(iconv.encode(line7, 'gbk')); socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗 // 支付方式 const line8 = padLeftAlign('支付方式', 10) + padRightAlign(data.payType, 38) + '\n'; socket.write(iconv.encode(line8, 'gbk')); // 分割线 const line9 = createDivider(lineWidth, 'thin') + '\n'; socket.write(iconv.encode(line9, 'gbk')); // ============== 尾部信息 ================ let bottom = '' bottom += padLeftAlign('操作员:', 8) + padLeftAlign(data.operator, 38) + '\n' bottom += padLeftAlign('打印时间:', 10) + padLeftAlign(dayjs(data.printTime).format('YYYY/M/D HH:mm:ss'), 38) + '\n' bottom += padLeftAlign('充值编号:', 10) + padLeftAlign(data.rechargeId || '-', 38) + '\n' bottom += createDivider(lineWidth, 'full') + '\n' bottom += '\n\n' bottom += '\n\n' socket.write(iconv.encode(bottom, 'gbk')) socket.write(Buffer.from([0x1D, 0x56, 0x00])) setTimeout(() => { socket.end() resolve('success') }, 400) } catch (err) { console.error(err) socket.destroy() resolve('error') } }) }) } // ========== 出入库单 ========== export function STOCK(printerIp, data) { return new Promise(resolve => { console.log('出入库单', data); const socket = new net.Socket() const lineWidth = 48 socket.setTimeout(8000) const title2M = { 'IN': '入库单', 'OUT': '出库单' } socket.connect(9100, printerIp, () => { try { setupPrinter(socket) // ============== 标题区域 ================ const title1 = data.shop_name || '' const title2 = title2M[data.type] // 核心修复:先写入空行清空打印机缓冲区,避免残留字符干扰居中起始位置 socket.write(iconv.encode('\n', 'gbk')); // 步骤1:重置打印机格式(避免残留格式影响) socket.write(Buffer.from([0x1B, 0x40])); // 步骤2:居中对齐标题 socket.write(Buffer.from([0x1B, 0x61, 0x01])); // 1 = center // 步骤3:设置标题1的格式(加粗+大号字体) socket.write(Buffer.from([0x1B, 0x21, 0x11])); // 字号放大(0x11 是常用放大值) socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗 socket.write(iconv.encode(title1 + '\n', 'gbk')); // 步骤4:恢复格式,打印标题2(常规字体) socket.write(Buffer.from([0x1B, 0x21, 0x00])); // 恢复默认字号 socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗 socket.write(iconv.encode(title2 + '\n\n', 'gbk')); socket.write(Buffer.from([0x1B, 0x61, 0x00])); // 0 = left // 打印信息区域 let infoData = '' infoData += padLeftAlign('入库时间:', 10) + padLeftAlign(dayjs(data.inStockTime).format('YYYY/MM/DD HH:mm:ss'), 38) + '\n' infoData += createDivider(lineWidth, 'thin') + '\n' socket.write(iconv.encode(infoData, 'gbk')) // ============== 盘点信息区域 ================ let tableInfo = '' tableInfo += padLeftAlign('耗材名称', 16) tableInfo += padLeftAlign('库存单位', 10) tableInfo += padLeftAlign('入库数量', 6) tableInfo += padRightAlign('金额', 12) + '\n' data.items.forEach(item => { const name = truncateByPrintWidth(item.consName, 18) const price = item.unit const num = item.stockNumber const total = item.amount tableInfo += padLeftAlign(name, 16) tableInfo += padLeftAlign(price, 10) tableInfo += padLeftAlign(num, 6) tableInfo += padRightAlign(total, 12) + '\n' }) tableInfo += '\n\n\n' socket.write(iconv.encode(tableInfo, 'gbk')) socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗 let line1 = '' line1 += padLeftAlign('总计', 16) line1 += padLeftAlign(`耗材${data.consCount}种`, 10) line1 += padLeftAlign(`数量${data.stockNumberCount}`, 8) line1 += padRightAlign(`金额${data.amountCount}`, 14) + '\n' socket.write(iconv.encode(line1, 'gbk')); socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗 // ============== 尾部信息 ================ let bottom = '' bottom += createDivider(lineWidth, 'thin') + '\n' bottom += padLeftAlign('操作员:', 8) + padLeftAlign(data.operator, 38) + '\n' bottom += padLeftAlign('打印时间:', 10) + padLeftAlign(dayjs(data.printTime).format('YYYY/M/D HH:mm:ss'), 38) + '\n' bottom += createDivider(lineWidth, 'full') + '\n' bottom += '\n\n' bottom += '\n\n' socket.write(iconv.encode(bottom, 'gbk')) socket.write(Buffer.from([0x1D, 0x56, 0x00])) setTimeout(() => { socket.end() resolve('success') }, 400) } catch (err) { console.error(err) socket.destroy() resolve('error') } }) }) } const CMD = { RESET: Buffer.from([0x1B, 0x40]), // 复位打印机 ALIGN_LEFT: Buffer.from([0x1B, 0x61, 0x00]), ALIGN_CENTER: Buffer.from([0x1B, 0x61, 0x01]), ALIGN_RIGHT: Buffer.from([0x1B, 0x61, 0x02]), BOLD_ON: Buffer.from([0x1B, 0x45, 0x01]), BOLD_OFF: Buffer.from([0x1B, 0x45, 0x00]), FONT_NORMAL: Buffer.from([0x1B, 0x21, 0x00]), FONT_BIG: Buffer.from([0x1B, 0x21, 0x11]), CUT: Buffer.from([0x1D, 0x56, 0x00]), }; // 核心函数:使用芯烨原生指令打印二维码 function printQRCode(socket, text) { try { // 1. 居中对齐 socket.write(Buffer.from([0x1B, 0x61, 0x01])); // ESC a 1 // 2. 设置二维码纠错等级 (L: 7%, M: 15%, Q: 25%, H: 30%) // 指令: GS ( k e p m // e=49(1), p=49(1), m=52(4) -> 纠错等级 H socket.write(Buffer.from([0x1D, 0x28, 0x6B, 0x04, 0x00, 0x31, 0x41, 0x34, 0x00])); // 3. 设置二维码模块大小 (即二维码的“颗粒”大小,80mm打印机建议设为 6-10) // 指令: GS ( k e p m n // n=6 (点大小) socket.write(Buffer.from([0x1D, 0x28, 0x6B, 0x03, 0x00, 0x31, 0x43, 0x0A])); // 4. 存储二维码数据 (这是最关键的一步) // 指令: GS ( k pL pH cn fn d1...dk const strBuf = Buffer.from(text, 'utf8'); // 数据内容 const len = strBuf.length + 3; // 数据长度 + 3 (cn + fn + 数据) const pL = len % 256; // 低位字节 const pH = Math.floor(len / 256); // 高位字节 // 构建指令头 const header = Buffer.from([0x1D, 0x28, 0x6B, pL, pH, 0x31, 0x50, 0x30]); socket.write(header); socket.write(strBuf); // 写入实际数据 // 5. 打印二维码 // 指令: GS ( k pL pH cn fn m // m=50(2) -> 打印 socket.write(Buffer.from([0x1D, 0x28, 0x6B, 0x03, 0x00, 0x31, 0x51, 0x30])); // 6. 换行,防止堵在一起 socket.write(Buffer.from([0x0A, 0x0A, 0x0A])); console.log('二维码指令发送成功'); } catch (e) { console.error('二维码打印错误:', e); socket.write(iconv.encode('二维码生成失败\n', 'gbk')); } } // 排队叫号单 export function CALL(printerIp, data) { return new Promise((resolve) => { console.log('排队取号单', data); const socket = new net.Socket(); const lineWidth = 48; socket.setTimeout(8000); socket.connect(9100, printerIp, () => { try { socket.write(CMD.RESET); // ============== 标题区域 ================ const title1 = data.callQueue.shopName || '店铺名称'; const title2 = `${data.callQueue.name} ${data.callQueue.callNum}`; const title3 = `前面还有${data.preNum}桌`; const title4 = `怕过号,扫一扫`; socket.write(CMD.ALIGN_CENTER); socket.write(CMD.FONT_BIG); socket.write(CMD.BOLD_ON); socket.write(iconv.encode(title1 + '\n', 'gbk')); socket.write(CMD.FONT_NORMAL); socket.write(CMD.BOLD_OFF); socket.write(CMD.ALIGN_CENTER); socket.write(iconv.encode(title2 + '\n', 'gbk')); socket.write(CMD.ALIGN_CENTER); socket.write(iconv.encode(title3 + '\n\n', 'gbk')); socket.write(CMD.ALIGN_CENTER); socket.write(iconv.encode(title4 + '\n\n', 'gbk')); socket.write(CMD.RESET); socket.write(CMD.ALIGN_CENTER); printQRCode(socket, data.callUrl); // 👈 用这个 socket.write(CMD.ALIGN_LEFT); // ============== 尾部信息 ================ let bottom = ''; bottom += createDivider(lineWidth, 'thin') + '\n'; bottom += padLeftAlign('描述:', 6) + padLeftAlign(data.callQueue.note) + '\n' // if (data.callTable.postponeNum == 1) { // bottom += padLeftAlign(`听到叫号请到前台,过号可顺延${data.callTable.isPostpone}桌`, 8) + '\n'; // } bottom += padLeftAlign('取号时间:', 10) + padLeftAlign(dayjs(data.callQueue.createTime).format('YYYY/M/D HH:mm:ss'), 38) + '\n'; bottom += createDivider(lineWidth, 'full') + '\n\n\n\n\n\n'; socket.write(iconv.encode(bottom, 'gbk')); socket.write(CMD.CUT); setTimeout(() => { socket.end(); resolve('success'); }, 800); } catch (err) { console.error('打印出错', err); socket.destroy(); resolve('error'); } }); socket.on('error', (err) => { console.error('Socket错误', err); resolve('error'); }); }); }