Files
cashier_desktop/electron/printService.js
2026-04-22 14:23:46 +08:00

1760 lines
88 KiB
JavaScript
Raw 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'
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');
});
});
}