Files
cashier_desktop/electron/printService.js
2026-04-03 15:35:48 +08:00

529 lines
28 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 网口打印机管理80mm 终极美化版 · 无多余间距最终版)
import net from 'net'
import iconv from 'iconv-lite'
// ======================== 全局工具函数 ========================
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
}
// ======================== 打印结算小票 ========================
export function printReceipt(printerIp, 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 = `结算单 #${order.orderInfo.orderNum}`
if (order.isBefore && !order.isGuest) {
title2 = `${order.isBefore ? '预' : ''}结算单 #${order.orderInfo.orderNum}`;
}
if (order.isGuest && order.isBefore) {
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.printerName || '未知打印机'}-${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) {
orderInfo += padLeftAlign('结账时间:', 10) + padLeftAlign(order.createdAt || '-', 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 => {
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'
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'))
// ======================== 付款信息 ========================
const payableLine = padLeftAlign('应付', 10) + padRightAlign(formatAmount(order.originAmount), 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])); // 取消加粗
socket.write(iconv.encode(createDivider(lineWidth, 'thin') + '\n', 'gbk'));
if (!order.isBefore) {
const paidLine = padLeftAlign('已付', 10) + padRightAlign(formatAmount(order.orderInfo.payAmount), 51) + '\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 = ''
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 printRefund(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 = order.title || '退款单'
// 核心修复:先写入空行清空打印机缓冲区,避免残留字符干扰居中起始位置
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.printerName || '未知打印机'}-${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])); // 取消加粗
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)
const formatAmount = (v) => Number(v || 0).toFixed(2)
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('交班周期:', 10) + padLeftAlign(`${data.loginTime}-${data.handoverTime}` || '-', 36) + '\n\n'
// basicInfo += createDivider(lineWidth, 'thin') + '\n'
// 收入明细 - 补充会员支付/充值字段
basicInfo += padLeftAlign('当班营业总额:', 10) + padLeftAlign(formatAmount(data.handAmount), 36) + '\n'
basicInfo += padLeftAlign('实际收款的支付方式', 10) + '\n'
basicInfo += padLeftAlign('现金', 10) + padRightAlign(formatAmount(data.cashAmount), 36) + '\n'
basicInfo += padLeftAlign('微信', 10) + padRightAlign(formatAmount(data.wechatAmount), 36) + '\n'
basicInfo += padLeftAlign('支付宝', 10) + padRightAlign(formatAmount(data.alipayAmount), 36) + '\n'
basicInfo += padLeftAlign('二维码收款', 10) + padRightAlign(formatAmount(data.vipPay), 36) + '\n'
basicInfo += padLeftAlign('扫码收款', 10) + padRightAlign(formatAmount(data.vipRecharge), 36) + '\n\n'
basicInfo += padLeftAlign('非实际收款的支付方式', 10) + '\n'
basicInfo += padLeftAlign('挂账', 10) + padRightAlign(formatAmount(data.creditAmount), 36) + '\n'
basicInfo += padLeftAlign('余额', 10) + padRightAlign(formatAmount(data.vipPay), 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.refundAmount), 36) + '\n'
refundInfo += createDivider(lineWidth, 'thin') + '\n'
socket.write(iconv.encode(refundInfo, 'gbk'))
let orderInfo = ''
const orderLabel = '订单(数量/订单总额)'
const orderValue = `${data.orderCount}/${formatAmount(data.orderAmount)}`
const orderLabelWidth = lineWidth - getPrintWidth(orderValue)
orderInfo += padLeftAlign(orderLabel, orderLabelWidth) + orderValue + '\n\n'
socket.write(iconv.encode(orderInfo, 'gbk'))
// ======================== 分类数据区域 ========================
let categoryInfo = ''
if (data.categoryDataList && data.categoryDataList.length) {
categoryInfo += centerAlign('销售数据', lineWidth) + '\n'
categoryInfo += createDivider(lineWidth, 'thin') + '\n'
// 分类表头
categoryInfo += padLeftAlign('名称', 24) + padLeftAlign('数量', 12) + padRightAlign('总计', 12) + '\n'
// categoryInfo += createDivider(lineWidth, 'thin') + '\n'
// 分类数据行
data.categoryDataList.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.printShop && data.productDataList && data.productDataList.length) {
productInfo += centerAlign('商品数据', lineWidth) + '\n'
productInfo += createDivider(lineWidth, 'thin') + '\n'
// 商品表头
productInfo += padLeftAlign('商品', 36) + padRightAlign('数量', 12) + '\n'
// productInfo += createDivider(lineWidth, 'thin') + '\n'
// 商品数据行
data.productDataList.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 incomeInfo = ''
// incomeInfo += centerAlign('【收支汇总】', lineWidth) + '\n'
// incomeInfo += createDivider(lineWidth, 'thin') + '\n'
// // 收支汇总项(补充挂账金额)
// const incomeItems = [
// { label: '快捷收款金额:', val: data.quickInAmount },
// { label: '退款金额:', val: data.refundAmount },
// { label: '总收入:', val: data.handAmount },
// { label: '挂账金额:', val: data.creditAmount },
// { label: '总订单数:', val: data.orderCount || 0 }
// ]
// incomeItems.forEach((item, index) => {
// const labelPart = padLeftAlign(item.label, 16)
// let valPart = ''
// // 总收入/总订单数加粗显示
// if (index === 2 || index === 4) {
// socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗
// valPart = padRightAlign(index === 4 ? item.val : formatAmount(item.val), 32)
// incomeInfo += labelPart + valPart + '\n'
// socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗
// } else {
// valPart = padRightAlign(formatAmount(item.val), 32)
// incomeInfo += labelPart + valPart + '\n'
// }
// })
// incomeInfo += createDivider(lineWidth, 'full') + '\n'
// socket.write(iconv.encode(incomeInfo, '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', () => { })
})
}