优化小票 新增存酒管理
This commit is contained in:
@@ -5,7 +5,7 @@ import axios from "axios";
|
||||
import os from "os";
|
||||
import fs from "fs";
|
||||
import { exec } from "child_process";
|
||||
import { printReceipt } from "./printService";
|
||||
import { printReceipt, printHandoverReceipt, printRefund } from "./printService";
|
||||
|
||||
// ===== 核心配置:单文件缓存 =====
|
||||
// 固定的缓存文件路径(永远只存这1个文件)
|
||||
@@ -107,6 +107,7 @@ app.whenReady().then(() => {
|
||||
});
|
||||
});
|
||||
|
||||
// 打印结算小票
|
||||
ipcMain.on('networkPrint', async (event, arg) => {
|
||||
try {
|
||||
let _parmas = JSON.parse(arg);
|
||||
@@ -120,6 +121,32 @@ app.whenReady().then(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// 打印交班小票
|
||||
ipcMain.on('printHandoverReceipt', async (event, arg) => {
|
||||
try {
|
||||
let _parmas = JSON.parse(arg);
|
||||
console.log(_parmas);
|
||||
await printHandoverReceipt(_parmas.printerIp, _parmas.handoverData);
|
||||
return { ok: true }
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return { ok: false, msg: e.message }
|
||||
}
|
||||
});
|
||||
|
||||
// 打印退菜单/退款小票
|
||||
ipcMain.on('printRefund', async (event, arg) => {
|
||||
try {
|
||||
let _parmas = JSON.parse(arg);
|
||||
// console.log(_parmas);
|
||||
await printRefund(_parmas.printerIp, _parmas.orderData);
|
||||
return { ok: true }
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return { ok: false, msg: e.message }
|
||||
}
|
||||
});
|
||||
|
||||
app.on("activate", () => {
|
||||
// 在 macOS 系统内, 如果没有已开启的应用窗口
|
||||
// 点击托盘图标时通常会重新创建一个新窗口
|
||||
|
||||
@@ -1,100 +1,529 @@
|
||||
// 网口打印机管理(80mm 终极美化版 · 无多余间距最终版)
|
||||
import net from 'net'
|
||||
import iconv from 'iconv-lite'
|
||||
|
||||
// ======================== 全局工具函数 ========================
|
||||
const getPrintWidth = (str) => {
|
||||
let width = 0
|
||||
const s = String(str || '')
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
const char = s.charAt(i)
|
||||
if (/[\u4e00-\u9fa5\u3000-\u303f\uff00-\uffef]/.test(char)) {
|
||||
width += 2
|
||||
} else {
|
||||
width += 1
|
||||
}
|
||||
}
|
||||
return width
|
||||
}
|
||||
|
||||
const padLeftAlign = (str, targetWidth) => {
|
||||
const s = String(str || '')
|
||||
const currentWidth = getPrintWidth(s)
|
||||
const spaceNum = Math.max(0, targetWidth - currentWidth)
|
||||
return s + ' '.repeat(spaceNum)
|
||||
}
|
||||
|
||||
const padRightAlign = (str, targetWidth) => {
|
||||
const s = String(str || '')
|
||||
const currentWidth = getPrintWidth(s)
|
||||
const spaceNum = Math.max(0, targetWidth - currentWidth)
|
||||
return ' '.repeat(spaceNum) + s
|
||||
}
|
||||
|
||||
const truncateByPrintWidth = (str, maxWidth) => {
|
||||
const s = String(str || '')
|
||||
let width = 0
|
||||
let result = ''
|
||||
for (const char of s) {
|
||||
const charWidth = /[\u4e00-\u9fa5\u3000-\u303f\uff00-\uffef]/.test(char) ? 2 : 1
|
||||
if (width + charWidth > maxWidth) break
|
||||
result += char
|
||||
width += charWidth
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const centerAlign = (str, lineWidth = 48) => {
|
||||
const s = String(str || '').trim(); // 去除首尾空格,避免干扰
|
||||
const currentWidth = getPrintWidth(s);
|
||||
if (currentWidth >= lineWidth) return s; // 内容超宽时直接返回,不居中
|
||||
|
||||
const totalPad = lineWidth - currentWidth;
|
||||
const leftPad = Math.floor(totalPad / 2);
|
||||
const rightPad = totalPad - leftPad; // 避免奇数宽度时总长度超行宽
|
||||
|
||||
// 用空格填充左右,保证总宽度严格等于 lineWidth
|
||||
return ' '.repeat(leftPad) + s + ' '.repeat(rightPad);
|
||||
};
|
||||
|
||||
const createDivider = (lineWidth = 48, type = 'full') => {
|
||||
const fullChar = '═'
|
||||
const thinChar = '─'
|
||||
const char = type === 'full' ? fullChar : thinChar
|
||||
return char.repeat(Math.ceil(lineWidth / 2)).substring(0, lineWidth)
|
||||
}
|
||||
|
||||
const setupPrinter = (socket, options = {}) => {
|
||||
socket.write(Buffer.from([0x1B, 0x40]))
|
||||
if (options.fontSize) {
|
||||
socket.write(Buffer.from([0x1B, 0x21, options.fontSize]))
|
||||
}
|
||||
if (options.bold) {
|
||||
socket.write(Buffer.from([0x1B, 0x45, options.bold ? 0x01 : 0x00]))
|
||||
}
|
||||
}
|
||||
|
||||
// 用餐模式 堂食 dine-in 外带 take-out 外卖 take-away
|
||||
const dineModeFilter = t => {
|
||||
if (t === 'dine-in') return '堂食'
|
||||
if (t === 'take-out') return '外带'
|
||||
if (t === 'take-away') return '外卖'
|
||||
return t
|
||||
}
|
||||
|
||||
// ======================== 打印结算小票 ========================
|
||||
export function printReceipt(printerIp, order) {
|
||||
return new Promise((resolve) => {
|
||||
const socket = new net.Socket()
|
||||
const lineWidth = 48
|
||||
socket.setTimeout(8000)
|
||||
|
||||
const formatAmount = (amount) => Number(amount || 0).toFixed(2)
|
||||
|
||||
socket.connect(9100, printerIp, () => {
|
||||
// 打印机复位
|
||||
socket.write(Buffer.from([0x1B, 0x40]))
|
||||
try {
|
||||
setupPrinter(socket)
|
||||
|
||||
// ==========================================
|
||||
// 全局配置:58mm 一行固定 32 个字符
|
||||
// ==========================================
|
||||
const lineWidth = 32
|
||||
const line = '================================'.substring(0, lineWidth)
|
||||
const lineMin = '--------------------------------'.substring(0, lineWidth)
|
||||
// ======================== 标题区域(彻底修复居中问题!) ========================
|
||||
const title1 = order.shop_name || '';
|
||||
let title2 = `结算单 #${order.orderInfo.orderNum}`
|
||||
if (order.isBefore && !order.isGuest) {
|
||||
title2 = `${order.isBefore ? '预' : ''}结算单 #${order.orderInfo.orderNum}`;
|
||||
}
|
||||
if (order.isGuest && order.isBefore) {
|
||||
title2 = `客看单 #${order.orderInfo.orderNum}`;
|
||||
}
|
||||
|
||||
// 居中函数
|
||||
const center = (str) => {
|
||||
const s = String(str)
|
||||
const len = s.length
|
||||
const pad = Math.max(0, Math.floor((lineWidth - len) / 2))
|
||||
return ' '.repeat(pad) + s + '\n'
|
||||
// 核心修复:先写入空行清空打印机缓冲区,避免残留字符干扰居中起始位置
|
||||
socket.write(iconv.encode('\n', 'gbk'));
|
||||
|
||||
// 步骤1:重置打印机格式(避免残留格式影响)
|
||||
socket.write(Buffer.from([0x1B, 0x40]));
|
||||
// 步骤2:居中对齐标题
|
||||
socket.write(Buffer.from([0x1B, 0x61, 0x01])); // 1 = center
|
||||
// 步骤3:设置标题1的格式(加粗+大号字体)
|
||||
socket.write(Buffer.from([0x1B, 0x21, 0x11])); // 字号放大(0x11 是常用放大值)
|
||||
socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗
|
||||
socket.write(iconv.encode(title1 + '\n', 'gbk'));
|
||||
|
||||
// 步骤4:恢复格式,打印标题2(常规字体)
|
||||
socket.write(Buffer.from([0x1B, 0x21, 0x00])); // 恢复默认字号
|
||||
socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗
|
||||
socket.write(iconv.encode(title2 + '\n\n', 'gbk'));
|
||||
socket.write(Buffer.from([0x1B, 0x61, 0x00])); // 0 = left
|
||||
|
||||
// ======================== 订单信息(修复显示+换行问题) ========================
|
||||
const tableLine = padLeftAlign('桌台号:', 8) + padLeftAlign(order.orderInfo.tableName || '无', 14) + '\n'
|
||||
socket.write(Buffer.from([0x1B, 0x21, 0x00])); // 正常字号
|
||||
socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗
|
||||
socket.write(iconv.encode(tableLine, 'gbk'));
|
||||
socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗
|
||||
|
||||
let orderInfo = ''
|
||||
|
||||
// 修复:打印机-用餐模式 + 就餐人数 展示逻辑
|
||||
const dineModeText = dineModeFilter(order.orderInfo.dineMode)
|
||||
// 1. 拼接打印机+用餐模式文本,空值兜底
|
||||
const printerDineText = `${order.printerName || '未知打印机'}-${dineModeText || '未知模式'}`
|
||||
// 2. 截断超长文本(左侧分配20宽度,保证不超限)
|
||||
const truncatedPrinterDine = truncateByPrintWidth(printerDineText, 20)
|
||||
// 3. 左侧左对齐填充20宽度(保证占满分配空间,不浪费空白)
|
||||
const leftPart = padLeftAlign(truncatedPrinterDine, 20)
|
||||
// 4. 右侧就餐人数:空值兜底为“无”,右对齐填充28宽度(20+28=48,刚好行宽)
|
||||
const seatNumText = order.orderInfo.seatNum || '0'
|
||||
const rightPart = padRightAlign(`${seatNumText}人`, 28) // 加标签更清晰,也可只显示数字
|
||||
|
||||
orderInfo += leftPart + rightPart + '\n'
|
||||
if (!order.isBefore) {
|
||||
orderInfo += padLeftAlign('结账时间:', 10) + padLeftAlign(order.createdAt || '-', 38) + '\n'
|
||||
}
|
||||
orderInfo += createDivider(lineWidth, 'thin') + '\n'
|
||||
|
||||
// ======================== 商品表格 ========================
|
||||
orderInfo += padLeftAlign('品名', 18)
|
||||
orderInfo += padLeftAlign('单价', 8)
|
||||
orderInfo += padLeftAlign('数量', 6)
|
||||
orderInfo += padRightAlign('小计', 16) + '\n'
|
||||
orderInfo += createDivider(lineWidth, 'thin') + '\n'
|
||||
|
||||
order.carts.forEach(item => {
|
||||
const name = truncateByPrintWidth(item.name || '未知商品', 18)
|
||||
const price = formatAmount(item.salePrice)
|
||||
const num = item.number || 1
|
||||
const total = formatAmount(item.totalAmount)
|
||||
orderInfo += padLeftAlign(name, 18)
|
||||
orderInfo += padLeftAlign(price, 8)
|
||||
orderInfo += padLeftAlign(num, 6)
|
||||
orderInfo += padRightAlign(total, 16) + '\n'
|
||||
})
|
||||
|
||||
orderInfo += createDivider(lineWidth, 'thin') + '\n'
|
||||
orderInfo += padLeftAlign('原价', 10) + padRightAlign(formatAmount(order.originAmount), 38) + '\n'
|
||||
orderInfo += padLeftAlign('优惠金额', 10) + padRightAlign(`-${formatAmount(order.discountAllAmount)}`, 38) + '\n'
|
||||
orderInfo += padLeftAlign('备注:', 6) + padLeftAlign(order.remark || '无', 38) + '\n'
|
||||
orderInfo += createDivider(lineWidth, 'thin') + '\n'
|
||||
socket.write(iconv.encode(orderInfo, 'gbk'))
|
||||
|
||||
// ======================== 付款信息 ========================
|
||||
const payableLine = padLeftAlign('应付', 10) + padRightAlign(formatAmount(order.originAmount), 51) + '\n'
|
||||
socket.write(Buffer.from([0x1B, 0x21, 0x11])); // 放大字号
|
||||
socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗
|
||||
socket.write(iconv.encode(payableLine, 'gbk'));
|
||||
socket.write(Buffer.from([0x1B, 0x21, 0x00])); // 恢复默认字号
|
||||
socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗
|
||||
socket.write(iconv.encode(createDivider(lineWidth, 'thin') + '\n', 'gbk'));
|
||||
if (!order.isBefore) {
|
||||
const paidLine = padLeftAlign('已付', 10) + padRightAlign(formatAmount(order.orderInfo.payAmount), 51) + '\n'
|
||||
socket.write(Buffer.from([0x1B, 0x21, 0x11]));
|
||||
socket.write(Buffer.from([0x1B, 0x45, 0x01]));
|
||||
socket.write(iconv.encode(paidLine, 'gbk'));
|
||||
socket.write(Buffer.from([0x1B, 0x21, 0x00]));
|
||||
socket.write(Buffer.from([0x1B, 0x45, 0x00]));
|
||||
socket.write(iconv.encode(createDivider(lineWidth, 'thin') + '\n', 'gbk'));
|
||||
}
|
||||
|
||||
// ======================== 底部 ========================
|
||||
let bottom = ''
|
||||
bottom += padLeftAlign('操作员:', 8) + padLeftAlign(order.loginAccount || '无', 38) + '\n'
|
||||
bottom += padLeftAlign('打印时间:', 10) + padLeftAlign(new Date().toLocaleString(), 38) + '\n'
|
||||
bottom += padLeftAlign('订单号:', 8) + padLeftAlign(order.orderInfo.orderNo || '-', 38) + '\n'
|
||||
bottom += createDivider(lineWidth, 'full') + '\n'
|
||||
bottom += '\n\n'
|
||||
bottom += '\n\n'
|
||||
|
||||
socket.write(iconv.encode(bottom, 'gbk'))
|
||||
socket.write(Buffer.from([0x1D, 0x56, 0x00]))
|
||||
|
||||
setTimeout(() => {
|
||||
socket.end()
|
||||
resolve('success')
|
||||
}, 400)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
socket.destroy()
|
||||
resolve('error')
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 标题:店名 + 结算单 + 桌号 → 全部居中对齐
|
||||
// ==========================================
|
||||
socket.write(Buffer.from([0x1B, 0x21, 0x11])) // 放大店名
|
||||
socket.write(iconv.encode(center(order.shop_name), 'gbk'))
|
||||
socket.write(Buffer.from([0x1B, 0x21, 0x00])) // 恢复正常
|
||||
|
||||
socket.write(iconv.encode(center(`结算单 #${order.orderInfo.orderNum}`), 'gbk'))
|
||||
socket.write(iconv.encode(center(`桌号:${order.orderInfo.tableName || ''}`), 'gbk'))
|
||||
socket.write(iconv.encode(line + '\n', 'gbk'))
|
||||
|
||||
// ==========================================
|
||||
// 订单信息(全部撑满宽度)
|
||||
// ==========================================
|
||||
let content = ''
|
||||
content += `订单号:${order.orderInfo.orderNo}\n`
|
||||
content += `交易时间:${order.createdAt}\n`
|
||||
content += `收银员:${order.loginAccount}\n`
|
||||
content += lineMin + '\n'
|
||||
|
||||
// ==========================================
|
||||
// 商品表格:完全对齐、撑满宽度
|
||||
// ==========================================
|
||||
content += '品名 单价 数量 小计\n'
|
||||
content += lineMin + '\n'
|
||||
|
||||
order.carts.forEach(item => {
|
||||
let name = String(item.name || '').substring(0, 10).padEnd(10, ' ')
|
||||
let price = String(item.salePrice || '0.00').padStart(6, ' ')
|
||||
let num = String(item.number || 1).padStart(3, ' ')
|
||||
let total = String(item.totalAmount || '0.00').padStart(3, ' ')
|
||||
content += name + price + num + total + '\n'
|
||||
})
|
||||
|
||||
content += lineMin + '\n'
|
||||
|
||||
// ==========================================
|
||||
// 金额信息
|
||||
// ==========================================
|
||||
content += `原价:${order.originAmount}\n`
|
||||
content += `折扣:${order.discountAllAmount}\n`
|
||||
|
||||
// 发送前面内容
|
||||
socket.write(iconv.encode(content, 'gbk'))
|
||||
|
||||
// 实付放大
|
||||
socket.write(Buffer.from([0x1B, 0x21, 0x11]))
|
||||
socket.write(iconv.encode(`实付:${order.orderAmount}\n`, 'gbk'))
|
||||
socket.write(Buffer.from([0x1B, 0x21, 0x00]))
|
||||
|
||||
// 底部信息
|
||||
let bottom = ''
|
||||
bottom += `备注:${order.remark || '无'}\n`
|
||||
bottom += `打印时间:${order.printTime}\n`
|
||||
bottom += line + '\n'
|
||||
bottom += '\n\n\n\n\n\n'
|
||||
|
||||
socket.write(iconv.encode(bottom, 'gbk'))
|
||||
|
||||
// 只切纸一次
|
||||
socket.write(Buffer.from([0x1D, 0x56, 0x00]))
|
||||
|
||||
setTimeout(() => {
|
||||
socket.end()
|
||||
resolve('ok')
|
||||
}, 400)
|
||||
})
|
||||
|
||||
socket.on('error', () => { })
|
||||
socket.on('error', () => resolve('error'))
|
||||
socket.on('timeout', () => resolve('timeout'))
|
||||
socket.on('close', () => { })
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// ======================== 打印退菜单/退款小票 ========================
|
||||
export function printRefund(printerIp, order) {
|
||||
console.log(JSON.stringify(order));
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const socket = new net.Socket()
|
||||
const lineWidth = 48
|
||||
socket.setTimeout(8000)
|
||||
|
||||
const formatAmount = (amount) => Number(amount || 0).toFixed(2)
|
||||
|
||||
socket.connect(9100, printerIp, () => {
|
||||
try {
|
||||
setupPrinter(socket)
|
||||
|
||||
// ======================== 标题区域(彻底修复居中问题!) ========================
|
||||
const title1 = order.shop_name || '';
|
||||
let title2 = order.title || '退款单'
|
||||
|
||||
// 核心修复:先写入空行清空打印机缓冲区,避免残留字符干扰居中起始位置
|
||||
socket.write(iconv.encode('\n', 'gbk'));
|
||||
|
||||
// 步骤1:重置打印机格式(避免残留格式影响)
|
||||
socket.write(Buffer.from([0x1B, 0x40]));
|
||||
// 步骤2:居中对齐标题
|
||||
socket.write(Buffer.from([0x1B, 0x61, 0x01])); // 1 = center
|
||||
// 步骤3:设置标题1的格式(加粗+大号字体)
|
||||
socket.write(Buffer.from([0x1B, 0x21, 0x11])); // 字号放大(0x11 是常用放大值)
|
||||
socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗
|
||||
socket.write(iconv.encode(title1 + '\n', 'gbk'));
|
||||
|
||||
// 步骤4:恢复格式,打印标题2(常规字体)
|
||||
socket.write(Buffer.from([0x1B, 0x21, 0x00])); // 恢复默认字号
|
||||
socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗
|
||||
socket.write(iconv.encode(title2 + '\n\n', 'gbk'));
|
||||
socket.write(Buffer.from([0x1B, 0x61, 0x00])); // 0 = left
|
||||
|
||||
// ======================== 订单信息(修复显示+换行问题) ========================
|
||||
const tableLine = padLeftAlign('桌台号:', 8) + padLeftAlign(order.orderInfo.tableName || '无', 14) + '\n'
|
||||
socket.write(Buffer.from([0x1B, 0x21, 0x00])); // 正常字号
|
||||
socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗
|
||||
socket.write(iconv.encode(tableLine, 'gbk'));
|
||||
socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗
|
||||
|
||||
let orderInfo = ''
|
||||
|
||||
// 修复:打印机-用餐模式 + 就餐人数 展示逻辑
|
||||
const dineModeText = dineModeFilter(order.orderInfo.dineMode)
|
||||
// 1. 拼接打印机+用餐模式文本,空值兜底
|
||||
const printerDineText = `${order.printerName || '未知打印机'}-${dineModeText || '未知模式'}`
|
||||
// 2. 截断超长文本(左侧分配20宽度,保证不超限)
|
||||
const truncatedPrinterDine = truncateByPrintWidth(printerDineText, 20)
|
||||
// 3. 左侧左对齐填充20宽度(保证占满分配空间,不浪费空白)
|
||||
const leftPart = padLeftAlign(truncatedPrinterDine, 20)
|
||||
// 4. 右侧就餐人数:空值兜底为“无”,右对齐填充28宽度(20+28=48,刚好行宽)
|
||||
const seatNumText = order.orderInfo.seatNum || '0'
|
||||
const rightPart = padRightAlign(`${seatNumText}人`, 28) // 加标签更清晰,也可只显示数字
|
||||
|
||||
if (order.carts.length > 0) {
|
||||
orderInfo += leftPart + rightPart + '\n'
|
||||
orderInfo += createDivider(lineWidth, 'thin') + '\n'
|
||||
|
||||
// ======================== 商品表格 ========================
|
||||
orderInfo += padLeftAlign('品名', 18)
|
||||
orderInfo += padLeftAlign('单价', 8)
|
||||
orderInfo += padLeftAlign('数量', 6)
|
||||
orderInfo += padRightAlign('小计', 16) + '\n'
|
||||
orderInfo += createDivider(lineWidth, 'thin') + '\n'
|
||||
|
||||
order.carts.forEach(item => {
|
||||
const name = truncateByPrintWidth(item.name || '未知商品', 18)
|
||||
const price = formatAmount(item.salePrice)
|
||||
const num = item.number || 1
|
||||
const total = formatAmount(item.totalAmount)
|
||||
orderInfo += padLeftAlign(name, 18)
|
||||
orderInfo += padLeftAlign(price, 8)
|
||||
orderInfo += padLeftAlign(num, 6)
|
||||
orderInfo += padRightAlign(total, 16) + '\n'
|
||||
})
|
||||
|
||||
orderInfo += createDivider(lineWidth, 'thin') + '\n'
|
||||
socket.write(iconv.encode(orderInfo, 'gbk'))
|
||||
}
|
||||
|
||||
// ======================== 付款信息 ========================
|
||||
let payableLine = ''
|
||||
payableLine += padLeftAlign('退款总计', 10) + padRightAlign(formatAmount(order.amount), 51) + '\n'
|
||||
socket.write(Buffer.from([0x1B, 0x21, 0x11])); // 放大字号
|
||||
socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗
|
||||
socket.write(iconv.encode(payableLine, 'gbk'));
|
||||
socket.write(Buffer.from([0x1B, 0x21, 0x00])); // 恢复默认字号
|
||||
socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗
|
||||
let refundInfo = ''
|
||||
refundInfo += padLeftAlign('退款方式:', 10) + padLeftAlign(order.refundMethod || '无', 38) + '\n'
|
||||
refundInfo += padLeftAlign('退款原因:', 10) + padLeftAlign(order.remark || '无', 38) + '\n'
|
||||
socket.write(iconv.encode(refundInfo, 'gbk'));
|
||||
socket.write(iconv.encode(createDivider(lineWidth, 'thin') + '\n', 'gbk'));
|
||||
|
||||
// ======================== 底部 ========================
|
||||
let bottom = ''
|
||||
bottom += padLeftAlign('操作员:', 8) + padLeftAlign(order.loginAccount || '无', 38) + '\n'
|
||||
bottom += padLeftAlign('打印时间:', 10) + padLeftAlign(new Date().toLocaleString(), 38) + '\n'
|
||||
bottom += padLeftAlign('原订单号:', 10) + padLeftAlign(order.orderInfo.orderNo || '-', 38) + '\n'
|
||||
bottom += createDivider(lineWidth, 'full') + '\n'
|
||||
bottom += '\n\n'
|
||||
bottom += '\n\n'
|
||||
|
||||
socket.write(iconv.encode(bottom, 'gbk'))
|
||||
socket.write(Buffer.from([0x1D, 0x56, 0x00]))
|
||||
|
||||
setTimeout(() => {
|
||||
socket.end()
|
||||
resolve('success')
|
||||
}, 400)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
socket.destroy()
|
||||
resolve('error')
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('error', () => resolve('error'))
|
||||
socket.on('timeout', () => resolve('timeout'))
|
||||
socket.on('close', () => { })
|
||||
})
|
||||
}
|
||||
|
||||
// ======================== 打印交班小票(完整版 · 补齐分类/商品数据) ========================
|
||||
export function printHandoverReceipt(printerIp, data) {
|
||||
return new Promise((resolve) => {
|
||||
const socket = new net.Socket()
|
||||
const lineWidth = 48
|
||||
socket.setTimeout(8000)
|
||||
|
||||
const formatAmount = (v) => Number(v || 0).toFixed(2)
|
||||
|
||||
socket.connect(9100, printerIp, () => {
|
||||
try {
|
||||
setupPrinter(socket)
|
||||
|
||||
// ======================== 标题区域(统一结算小票的居中修复逻辑) ========================
|
||||
const title1 = data.shopName || '店铺';
|
||||
const title2 = '交班小票';
|
||||
|
||||
// 核心修复:先写入空行清空打印机缓冲区,避免残留字符干扰居中起始位置
|
||||
socket.write(iconv.encode('\n', 'gbk'));
|
||||
|
||||
// 步骤1:重置打印机格式(避免残留格式影响)
|
||||
socket.write(Buffer.from([0x1B, 0x40]));
|
||||
// 步骤2:居中对齐标题
|
||||
socket.write(Buffer.from([0x1B, 0x61, 0x01])); // 1 = center
|
||||
// 步骤3:设置标题1的格式(加粗+大号字体)
|
||||
socket.write(Buffer.from([0x1B, 0x21, 0x11])); // 字号放大(0x11 是常用放大值)
|
||||
socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗
|
||||
socket.write(iconv.encode(title1 + '\n', 'gbk'));
|
||||
|
||||
// 步骤4:恢复格式,打印标题2(常规字体)
|
||||
socket.write(Buffer.from([0x1B, 0x21, 0x00])); // 恢复默认字号
|
||||
socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗
|
||||
socket.write(iconv.encode(title2 + '\n\n', 'gbk'));
|
||||
socket.write(Buffer.from([0x1B, 0x61, 0x00])); // 0 = left
|
||||
socket.write(iconv.encode(createDivider(lineWidth, 'thin') + '\n', 'gbk'));
|
||||
|
||||
// ======================== 基础信息区域(拆分当班/交班时间) ========================
|
||||
let basicInfo = ''
|
||||
// 优化:拆分当班时间和交班时间,空值兜底
|
||||
basicInfo += padLeftAlign('交班时间:', 10) + padLeftAlign(data.handoverTime || '-', 36) + '\n'
|
||||
basicInfo += padLeftAlign('收银员:', 8) + padLeftAlign(data.staffName || '-', 36) + '\n'
|
||||
basicInfo += padLeftAlign('交班周期:', 10) + padLeftAlign(`${data.loginTime}-${data.handoverTime}` || '-', 36) + '\n\n'
|
||||
|
||||
// basicInfo += createDivider(lineWidth, 'thin') + '\n'
|
||||
|
||||
// 收入明细 - 补充会员支付/充值字段
|
||||
basicInfo += padLeftAlign('当班营业总额:', 10) + padLeftAlign(formatAmount(data.handAmount), 36) + '\n'
|
||||
basicInfo += padLeftAlign('实际收款的支付方式', 10) + '\n'
|
||||
basicInfo += padLeftAlign('现金', 10) + padRightAlign(formatAmount(data.cashAmount), 36) + '\n'
|
||||
basicInfo += padLeftAlign('微信', 10) + padRightAlign(formatAmount(data.wechatAmount), 36) + '\n'
|
||||
basicInfo += padLeftAlign('支付宝', 10) + padRightAlign(formatAmount(data.alipayAmount), 36) + '\n'
|
||||
basicInfo += padLeftAlign('二维码收款', 10) + padRightAlign(formatAmount(data.vipPay), 36) + '\n'
|
||||
basicInfo += padLeftAlign('扫码收款', 10) + padRightAlign(formatAmount(data.vipRecharge), 36) + '\n\n'
|
||||
basicInfo += padLeftAlign('非实际收款的支付方式', 10) + '\n'
|
||||
basicInfo += padLeftAlign('挂账', 10) + padRightAlign(formatAmount(data.creditAmount), 36) + '\n'
|
||||
basicInfo += padLeftAlign('余额', 10) + padRightAlign(formatAmount(data.vipPay), 36) + '\n'
|
||||
basicInfo += createDivider(lineWidth, 'thin') + '\n'
|
||||
|
||||
socket.write(Buffer.from([0x1B, 0x21, 0x00])); // 正常字号
|
||||
socket.write(iconv.encode(basicInfo, 'gbk'))
|
||||
|
||||
let refundInfo = ''
|
||||
refundInfo += padLeftAlign('退款/退菜', 10) + '\n'
|
||||
refundInfo += padLeftAlign('退款金额', 10) + padRightAlign(formatAmount(data.refundAmount), 36) + '\n'
|
||||
refundInfo += padLeftAlign('退菜数量', 10) + padRightAlign(formatAmount(data.refundAmount), 36) + '\n'
|
||||
refundInfo += createDivider(lineWidth, 'thin') + '\n'
|
||||
socket.write(iconv.encode(refundInfo, 'gbk'))
|
||||
|
||||
let orderInfo = ''
|
||||
const orderLabel = '订单(数量/订单总额)'
|
||||
const orderValue = `${data.orderCount}/${formatAmount(data.orderAmount)}`
|
||||
const orderLabelWidth = lineWidth - getPrintWidth(orderValue)
|
||||
orderInfo += padLeftAlign(orderLabel, orderLabelWidth) + orderValue + '\n\n'
|
||||
socket.write(iconv.encode(orderInfo, 'gbk'))
|
||||
|
||||
// ======================== 分类数据区域 ========================
|
||||
let categoryInfo = ''
|
||||
if (data.categoryDataList && data.categoryDataList.length) {
|
||||
categoryInfo += centerAlign('销售数据', lineWidth) + '\n'
|
||||
categoryInfo += createDivider(lineWidth, 'thin') + '\n'
|
||||
// 分类表头
|
||||
categoryInfo += padLeftAlign('名称', 24) + padLeftAlign('数量', 12) + padRightAlign('总计', 12) + '\n'
|
||||
// categoryInfo += createDivider(lineWidth, 'thin') + '\n'
|
||||
// 分类数据行
|
||||
data.categoryDataList.forEach(item => {
|
||||
const catName = truncateByPrintWidth(item.categoryName || '未知分类', 24)
|
||||
const num = item.num || 0
|
||||
const amount = formatAmount(item.amount)
|
||||
categoryInfo += padLeftAlign(catName, 24) + padLeftAlign(num, 12) + padRightAlign(amount, 12) + '\n'
|
||||
})
|
||||
categoryInfo += createDivider(lineWidth, 'thin') + '\n'
|
||||
}
|
||||
socket.write(iconv.encode(categoryInfo, 'gbk'))
|
||||
|
||||
// ======================== 商品数据区域 ========================
|
||||
let productInfo = ''
|
||||
if (data.printShop && data.productDataList && data.productDataList.length) {
|
||||
productInfo += centerAlign('商品数据', lineWidth) + '\n'
|
||||
productInfo += createDivider(lineWidth, 'thin') + '\n'
|
||||
// 商品表头
|
||||
productInfo += padLeftAlign('商品', 36) + padRightAlign('数量', 12) + '\n'
|
||||
// productInfo += createDivider(lineWidth, 'thin') + '\n'
|
||||
// 商品数据行
|
||||
data.productDataList.forEach(item => {
|
||||
const prodName = truncateByPrintWidth(item.productName || '未知商品', 36)
|
||||
const num = item.num || 0
|
||||
productInfo += padLeftAlign(prodName, 36) + padRightAlign(num, 12) + '\n'
|
||||
})
|
||||
productInfo += createDivider(lineWidth, 'thin') + '\n'
|
||||
}
|
||||
socket.write(iconv.encode(productInfo, 'gbk'))
|
||||
|
||||
// ======================== 收入明细区域(优化对齐和格式) ========================
|
||||
// let incomeInfo = ''
|
||||
// incomeInfo += centerAlign('【收支汇总】', lineWidth) + '\n'
|
||||
// incomeInfo += createDivider(lineWidth, 'thin') + '\n'
|
||||
|
||||
// // 收支汇总项(补充挂账金额)
|
||||
// const incomeItems = [
|
||||
// { label: '快捷收款金额:', val: data.quickInAmount },
|
||||
// { label: '退款金额:', val: data.refundAmount },
|
||||
// { label: '总收入:', val: data.handAmount },
|
||||
// { label: '挂账金额:', val: data.creditAmount },
|
||||
// { label: '总订单数:', val: data.orderCount || 0 }
|
||||
// ]
|
||||
|
||||
// incomeItems.forEach((item, index) => {
|
||||
// const labelPart = padLeftAlign(item.label, 16)
|
||||
// let valPart = ''
|
||||
// // 总收入/总订单数加粗显示
|
||||
// if (index === 2 || index === 4) {
|
||||
// socket.write(Buffer.from([0x1B, 0x45, 0x01])); // 加粗
|
||||
// valPart = padRightAlign(index === 4 ? item.val : formatAmount(item.val), 32)
|
||||
// incomeInfo += labelPart + valPart + '\n'
|
||||
// socket.write(Buffer.from([0x1B, 0x45, 0x00])); // 取消加粗
|
||||
// } else {
|
||||
// valPart = padRightAlign(formatAmount(item.val), 32)
|
||||
// incomeInfo += labelPart + valPart + '\n'
|
||||
// }
|
||||
// })
|
||||
|
||||
// incomeInfo += createDivider(lineWidth, 'full') + '\n'
|
||||
// socket.write(iconv.encode(incomeInfo, 'gbk'))
|
||||
|
||||
// ======================== 底部区域(统一结算小票风格) ========================
|
||||
let bottomInfo = ''
|
||||
bottomInfo += padLeftAlign('打印时间:', 12) + padRightAlign(data.printTime || new Date().toLocaleString(), 36) + '\n'
|
||||
bottomInfo += createDivider(lineWidth, 'full') + '\n'
|
||||
// 增加空行,避免打印内容紧贴纸边
|
||||
bottomInfo += '\n\n'
|
||||
bottomInfo += '\n\n'
|
||||
|
||||
socket.write(iconv.encode(bottomInfo, 'gbk'))
|
||||
// 切纸指令(统一结算小票的切纸逻辑)
|
||||
socket.write(Buffer.from([0x1D, 0x56, 0x00]))
|
||||
|
||||
// 统一延迟关闭连接,保证数据完整发送
|
||||
setTimeout(() => {
|
||||
socket.end()
|
||||
resolve('success')
|
||||
}, 400)
|
||||
} catch (err) {
|
||||
console.error('打印交班小票异常:', err)
|
||||
socket.destroy()
|
||||
resolve('error')
|
||||
}
|
||||
})
|
||||
|
||||
// 统一错误处理(对齐结算小票的错误处理逻辑)
|
||||
socket.on('error', (err) => {
|
||||
console.error('打印机连接错误:', err)
|
||||
resolve('error')
|
||||
})
|
||||
socket.on('timeout', () => resolve('timeout'))
|
||||
socket.on('close', () => { })
|
||||
socket.on('timeout', () => socket.destroy())
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user