1760 lines
88 KiB
JavaScript
1760 lines
88 KiB
JavaScript
// 网口打印机管理(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');
|
||
});
|
||
});
|
||
} |