<template>
|
<view class="container">
|
<cu-custom :bgColor="NavBarColor" :isBack="true" backRouterName="index">
|
<block slot="backText">返回</block>
|
<block slot="content">网络打印机配置</block>
|
</cu-custom>
|
|
<!-- 打印机设置区域 -->
|
<view class="setting-area">
|
<view class="input-item">
|
<text>打印机IP:</text>
|
<uni-data-select v-model="printerIP" :localdata="printerList" @change="changePrinterList"
|
placeholder="请选择" />
|
<!-- <uni-easyinput v-model="printerIP" placeholder="例如: 192.168.1.100" /> -->
|
</view>
|
<view class="input-item">
|
<text>端口号:</text>
|
<uni-easyinput v-model="printerPort" type="number" placeholder="默认: 9100" value="9100" />
|
</view>
|
<view class="input-item">
|
<text>打印机型号:</text>
|
<uni-data-select v-model="printerModel" :localdata="printerModels" placeholder="选择打印机型号" />
|
</view>
|
<!-- 平台限制提示 -->
|
<view class="platform-tip" v-if="isMini || isH5">
|
⚠️ 小程序/H5 不支持直接TCP连接,需通过后端中转
|
</view>
|
<button @click="connectPrinter" :disabled="isConnected || (isMini || isH5)" class="btn primary">
|
连接打印机(仅APP支持)
|
</button>
|
<button @click="connectViaBackend" :disabled="isConnected" class="btn primary-outline"
|
v-if="isMini || isH5">
|
通过后端连接打印机
|
</button>
|
<button @click="disconnectPrinter" :disabled="!isConnected" class="btn danger">
|
断开连接
|
</button>
|
<button @click="testPrint" :disabled="!isConnected" class="btn test">
|
测试打印(ZPL指令)
|
</button>
|
</view>
|
|
<!-- 打印内容区域 -->
|
<view class="print-area" v-if="isConnected">
|
<text class="print-title">选择打印模板:</text>
|
<button @click="printStockLabel" class="btn print">打印成品入库标签</button>
|
<button @click="printTransferOrder" class="btn print">打印移库单</button>
|
<button @click="printInspectionForm(1)" class="btn print">打印检验单A</button>
|
<button @click="printInspectionForm(2)" class="btn print">打印检验单B</button>
|
</view>
|
|
<!-- 状态显示 -->
|
<view class="status">
|
连接状态: {{ isConnected ? '已连接(TCP)' : '未连接' }}
|
</view>
|
<view class="log" v-if="logList.length > 0">
|
<text class="log-title">操作日志:</text>
|
<view v-for="(log, index) in logList" :key="index" class="log-item">
|
{{ log }}
|
</view>
|
</view>
|
</view>
|
</template>
|
|
<script>
|
// 引入设备信息判断平台(APP/小程序/H5)
|
import {
|
getSystemInfo,
|
isApp,
|
isMini,
|
isH5
|
} from 'common/util/system.js';
|
import PrinterCommands from 'common/util/printer-commands.js';
|
|
export default {
|
name: 'NetworkPrinter',
|
data() {
|
return {
|
printerList: [],
|
NavBarColor: this.NavBarColor,
|
printerIP: '', // 斑马打印机IP(需替换为实际值)
|
printerPort: '9100', // 斑马默认TCP端口
|
printerModel: 'zebra', // 默认为斑马系列
|
printerModels: [{
|
value: 'epson',
|
text: '爱普生(EPSON)系列'
|
},
|
{
|
value: 'zebra',
|
text: '斑马(ZEBRA)系列' // 选中斑马
|
},
|
{
|
value: 'citizen',
|
text: '西铁城(CITIZEN)系列'
|
},
|
{
|
value: 'generic',
|
text: '通用ESC/POS兼容'
|
}
|
],
|
isConnected: false,
|
socketTask: null, // 原生TCP Socket实例(替换Socket.io)
|
logList: [],
|
isMini: false, // 是否为小程序
|
isH5: false, // 是否为H5
|
// 后端中转接口地址(小程序/H5使用)
|
url: {
|
list: 'base/printerConfig/queryUserPrinterConfigList'
|
},
|
backendPrintUrl: '/api/printer/print'
|
};
|
},
|
onLoad() {
|
// 初始化:判断当前平台
|
this.initPlatform();
|
// 加载缓存配置
|
this.loadPrinterConfig();
|
},
|
onUnload() {
|
// 页面销毁时断开连接
|
this.disconnectPrinter();
|
// 保存配置
|
this.savePrinterConfig();
|
},
|
created() {
|
this.getPrinterList()
|
},
|
methods: {
|
changePrinterList(e) {
|
this.printerIP = e;
|
console.log(this.printerIP)
|
},
|
getPrinterList() {
|
this.$http.get(this.url.list, {
|
params: {},
|
|
}).then(res => {
|
//设置列表数据
|
if (res.data.success) {
|
this.printerList = res.data.result
|
} else {
|
uni.showToast({
|
icon: "error",
|
title: res.data.message,
|
duration: 2000
|
});
|
|
}
|
}).catch(() => {
|
this.$tip.error("联网失败")
|
})
|
},
|
|
// 初始化平台(判断APP/小程序/H5)
|
initPlatform() {
|
// 直接调用导入的命名方法,不会报错
|
const systemInfo = getSystemInfo(); // 正确:调用命名导出的 getSystemInfo 函数
|
this.isApp = isApp(); // 正确:调用命名导出的 isApp 函数
|
this.isMini = isMini(); // 正确:调用命名导出的 isMini 函数
|
this.isH5 = isH5(); // 正确:调用命名导出的 isH5 函数
|
this.isMini = systemInfo.platform === 'mp-weixin' || systemInfo.platform === 'mp-alipay';
|
this.isH5 = systemInfo.platform === 'h5';
|
if (this.isMini || this.isH5) {
|
this.addLog(`当前平台:${this.isMini ? '小程序' : 'H5'},需通过后端中转`);
|
}
|
},
|
|
// 加载保存的打印机配置
|
loadPrinterConfig() {
|
const config = uni.getStorageSync('printerConfig');
|
if (config) {
|
this.printerIP = config.ip || this.printerIP;
|
this.printerPort = config.port || this.printerPort;
|
this.printerModel = config.model || this.printerModel;
|
}
|
},
|
|
// 保存打印机配置
|
savePrinterConfig() {
|
uni.setStorageSync('printerConfig', {
|
ip: this.printerIP,
|
port: this.printerPort,
|
model: this.printerModel
|
});
|
},
|
|
// 添加日志
|
addLog(message) {
|
const time = new Date().toLocaleTimeString();
|
this.logList.unshift(`[${time}] ${message}`);
|
if (this.logList.length > 20) this.logList.pop();
|
},
|
|
// 1. APP端:直接TCP连接斑马打印机(核心修改)
|
connectPrinter() {
|
if (!this.printerIP || !this.printerPort) {
|
uni.showToast({
|
title: '请输入IP和端口',
|
icon: 'none'
|
});
|
return;
|
}
|
|
// 先关闭已有连接
|
if (this.socketTask) this.disconnectPrinter();
|
|
this.addLog(`正在TCP连接:${this.printerIP}:${this.printerPort}`);
|
// 原生TCP Socket连接(type: 'tcp' 必须指定)
|
this.socketTask = uni.createSocket({
|
url: `tcp://${this.printerIP}:${this.printerPort}`,
|
type: 'tcp', // 明确指定为TCP连接(斑马打印机仅支持TCP)
|
success: () => {
|
this.addLog('Socket创建成功,等待连接...');
|
// 监听连接成功
|
this.socketTask.onOpen(() => {
|
this.isConnected = true;
|
this.addLog('斑马打印机连接成功(TCP)');
|
this.savePrinterConfig();
|
uni.showToast({
|
title: '连接成功',
|
icon: 'success'
|
});
|
});
|
// 监听连接错误
|
this.socketTask.onError((err) => {
|
this.isConnected = false;
|
this.addLog(`连接失败:${err.errMsg || '端口被占用或打印机离线'}`);
|
uni.showToast({
|
title: '连接失败,检查IP/端口',
|
icon: 'none'
|
});
|
});
|
// 监听连接断开
|
this.socketTask.onClose((res) => {
|
this.isConnected = false;
|
this.addLog(`连接断开:${res.reason || '未知原因'}`);
|
// 被动断开时尝试重连(可选)
|
if (res.code !== 1000) {
|
setTimeout(() => this.connectPrinter(), 3000);
|
}
|
});
|
},
|
fail: (err) => {
|
this.addLog(`创建Socket失败:${err.errMsg}`);
|
uni.showToast({
|
title: '创建连接失败',
|
icon: 'none'
|
});
|
}
|
});
|
},
|
|
// 2. 小程序/H5:通过后端中转连接(新增)
|
connectViaBackend() {
|
// 后端中转无需前端建立TCP,仅需保存配置并验证后端连通性
|
if (!this.printerIP || !this.printerPort) {
|
uni.showToast({
|
title: '请输入IP和端口',
|
icon: 'none'
|
});
|
return;
|
}
|
this.addLog(`通过后端验证打印机:${this.printerIP}:${this.printerPort}`);
|
// 调用后端接口验证打印机状态
|
uni.request({
|
url: '/api/printer/checkStatus',
|
method: 'POST',
|
data: {
|
ip: this.printerIP,
|
port: this.printerPort
|
},
|
success: (res) => {
|
if (res.data.success) {
|
this.isConnected = true; // 标记为“后端已连接”
|
this.addLog('后端连接打印机成功');
|
this.savePrinterConfig();
|
uni.showToast({
|
title: '后端连接成功',
|
icon: 'success'
|
});
|
} else {
|
this.addLog(`后端连接失败:${res.data.message}`);
|
uni.showToast({
|
title: res.data.message,
|
icon: 'none'
|
});
|
}
|
},
|
fail: (err) => {
|
this.addLog(`后端接口异常:${err.errMsg}`);
|
uni.showToast({
|
title: '后端接口不可用',
|
icon: 'none'
|
});
|
}
|
});
|
},
|
|
// 断开连接
|
disconnectPrinter() {
|
if (this.socketTask) {
|
this.socketTask.close({
|
success: () => {
|
this.isConnected = false;
|
this.socketTask = null;
|
this.addLog('已断开TCP连接');
|
uni.showToast({
|
title: '已断开连接',
|
icon: 'none'
|
});
|
}
|
});
|
} else if (this.isConnected && (this.isMini || this.isH5)) {
|
// 后端中转场景:通知后端断开
|
uni.request({
|
url: '/api/printer/disconnect'
|
});
|
this.isConnected = false;
|
this.addLog('已通知后端断开连接');
|
uni.showToast({
|
title: '已断开连接',
|
icon: 'none'
|
});
|
}
|
},
|
|
// 3. 发送ZPL指令(核心修改:直接发送二进制指令,替换Socket.io emit)
|
sendZplCommand(zplCode, retry = 2) {
|
return new Promise((resolve, reject) => {
|
// 区分APP直连和后端中转
|
if (this.isMini || this.isH5) {
|
// 小程序/H5:通过后端发送ZPL
|
uni.request({
|
url: this.backendPrintUrl,
|
method: 'POST',
|
data: {
|
ip: this.printerIP,
|
port: this.printerPort,
|
zplCode: zplCode
|
},
|
success: (res) => {
|
if (res.data.success) resolve(res.data);
|
else if (retry > 0) {
|
this.addLog(`后端打印失败,重试(${retry}次)...`);
|
setTimeout(() => this.sendZplCommand(zplCode, retry - 1).then(
|
resolve).catch(reject), 1000);
|
} else reject(new Error(res.data.message || '后端打印失败'));
|
},
|
fail: (err) => reject(new Error(`后端请求失败:${err.errMsg}`))
|
});
|
} else {
|
// APP端:直接TCP发送ZPL(二进制格式)
|
if (!this.socketTask || !this.isConnected) {
|
reject(new Error('未建立TCP连接'));
|
return;
|
}
|
// ZPL指令转为Uint8Array(避免编码问题)
|
const buffer = new Uint8Array(Buffer.from(zplCode));
|
this.socketTask.send({
|
data: buffer,
|
success: () => {
|
this.addLog('ZPL指令发送成功');
|
resolve({
|
success: true
|
});
|
},
|
fail: (err) => {
|
if (retry > 0) {
|
this.addLog(`指令发送失败,重试(${retry}次)...`);
|
setTimeout(() => this.sendZplCommand(zplCode, retry - 1).then(
|
resolve).catch(reject), 1000);
|
} else reject(new Error(`发送失败:${err.errMsg}`));
|
}
|
});
|
}
|
});
|
},
|
|
// 4. 构建斑马ZPL指令(核心修改:适配中文、二维码)
|
buildZpl(data, templateType) {
|
const commands = new PrinterCommands(this.printerModel);
|
// 斑马专用ZPL指令构建(覆盖PrinterCommands中对应方法)
|
switch (templateType) {
|
case 'test':
|
// 测试页:含中文和简单文本(解决乱码)
|
return `^XA^CI28^CFSIMSUN,30^FO50,50^FD斑马打印机测试页(中文)^FS^FO50,100^FD当前时间:${new Date().toLocaleString()}^FS^XZ`;
|
case 'stockLabel':
|
// 成品入库标签:含二维码(ZPL原生^BQ指令)
|
return `^XA
|
^CI28^CFSIMSUN,24 // UTF-8编码 + 中文字体(SIMSUN需提前上传到打印机)
|
^FO30,30^FD订单号:${data.orderNo}^FS
|
^FO30,70^FD客户:${data.customer}^FS
|
^FO30,110^FD物料号:${data.materialNo}^FS
|
^FO30,150^FD批次号:${data.batchNo}^FS
|
^FO30,190^FD规格:${data.spec}^FS
|
^FO30,230^FD数量:${data.quantity}件^FS
|
^FO220,30^BQN,2,10 // 二维码:QR Code,纠错等级2,大小10
|
^FDQA,物料:${data.materialNo},批次:${data.batchNo}^FS // QA前缀=QR Code,内容为物料+批次
|
^XZ`.replace(/\s+/g, ''); // 去除多余空格,避免解析错误
|
case 'transferOrder':
|
// 移库单:类似入库标签,调整字段
|
return `^XA
|
^CI28^CFSIMSUN,24
|
^FO30,30^FD移库单号:${data.orderNo}^FS
|
^FO30,70^FD客户:${data.customer}^FS
|
^FO30,110^FD物料号:${data.materialNo}^FS
|
^FO30,150^FD批次号:${data.batchNo}^FS
|
^FO30,190^FD工厂:${data.factory}^FS
|
^FO220,30^BQN,2,10
|
^FDQA,移库单:${data.orderNo},数量:${data.quantity}^FS
|
^XZ`.replace(/\s+/g, '');
|
case 'inspectionA':
|
case 'inspectionB':
|
// 检验单:多行检验项
|
let inspectionItems = '';
|
data.items.forEach((item, idx) => {
|
inspectionItems += `^FO30,${110 + idx * 40}^FD${item.name}:${item.result}^FS`;
|
});
|
return `^XA
|
^CI28^CFSIMSUN,24
|
^FO30,30^FD检验单号:${data.formNo}^FS
|
^FO30,70^FD检验员:${data.inspector}^FS
|
${inspectionItems}
|
^FO220,30^BQN,2,10
|
^FDQA,检验单:${data.formNo},物料:${data.materialNo}^FS
|
^XZ`.replace(/\s+/g, '');
|
default:
|
throw new Error('未知模板类型');
|
}
|
},
|
|
// 测试打印(ZPL指令)
|
async testPrint() {
|
try {
|
const zplCode = this.buildZpl({}, 'test');
|
await this.sendZplCommand(zplCode);
|
this.addLog('测试页打印成功(ZPL指令)');
|
uni.showToast({
|
title: '测试打印成功',
|
icon: 'success'
|
});
|
} catch (error) {
|
this.addLog(`测试打印失败:${error.message}`);
|
uni.showToast({
|
title: '打印失败',
|
icon: 'none'
|
});
|
}
|
},
|
|
// 打印成品入库标签(使用ZPL)
|
async printStockLabel() {
|
try {
|
const data = {
|
orderNo: '11263797',
|
customer: '吉利',
|
model: '6608268440',
|
materialNo: '120026829',
|
batchNo: '25219773',
|
spec: 'G3-700A',
|
quantity: 100,
|
date: '2023/06/07'
|
};
|
const zplCode = this.buildZpl(data, 'stockLabel');
|
await this.sendZplCommand(zplCode);
|
this.addLog('成品入库标签打印成功');
|
uni.showToast({
|
title: '打印成功',
|
icon: 'success'
|
});
|
} catch (error) {
|
this.addLog(`打印失败:${error.message}`);
|
uni.showToast({
|
title: '打印失败',
|
icon: 'none'
|
});
|
}
|
},
|
|
// 打印移库单(使用ZPL)
|
async printTransferOrder() {
|
try {
|
const data = {
|
orderNo: '112379',
|
productModel: 'G-639',
|
customer: '东方日产',
|
materialNo: '120047854',
|
customerModel: '4200-51354',
|
batchNo: '25159847',
|
factory: '双林新火炬工厂',
|
quantity: 73
|
};
|
const zplCode = this.buildZpl(data, 'transferOrder');
|
await this.sendZplCommand(zplCode);
|
this.addLog('移库单打印成功');
|
uni.showToast({
|
title: '打印成功',
|
icon: 'success'
|
});
|
} catch (error) {
|
this.addLog(`打印失败:${error.message}`);
|
uni.showToast({
|
title: '打印失败',
|
icon: 'none'
|
});
|
}
|
},
|
|
// 打印检验单(使用ZPL)
|
async printInspectionForm(type) {
|
try {
|
const data = {
|
formNo: type === 1 ? 'JYD-20250801' : 'JYD-20250802',
|
materialNo: '120026829',
|
batchNo: '25219773',
|
inspector: '张三',
|
date: new Date().toLocaleDateString(),
|
items: [{
|
name: '外观',
|
result: '合格'
|
},
|
{
|
name: '尺寸',
|
result: '合格'
|
},
|
{
|
name: '性能',
|
result: type === 1 ? '合格' : '待检'
|
}
|
]
|
};
|
const zplCode = this.buildZpl(data, `inspection${type === 1 ? 'A' : 'B'}`);
|
await this.sendZplCommand(zplCode);
|
this.addLog(`检验单${type}打印成功`);
|
uni.showToast({
|
title: '打印成功',
|
icon: 'success'
|
});
|
} catch (error) {
|
this.addLog(`打印失败:${error.message}`);
|
uni.showToast({
|
title: '打印失败',
|
icon: 'none'
|
});
|
}
|
}
|
}
|
};
|
</script>
|
|
|
<style scoped>
|
.platform-tip {
|
color: #FF3B30;
|
font-size: 24rpx;
|
margin-bottom: 15rpx;
|
padding: 10rpx;
|
background-color: #FFF8F8;
|
border-radius: 8rpx;
|
}
|
|
.btn.primary-outline {
|
background-color: #fff;
|
color: #007AFF;
|
border: 1rpx solid #007AFF;
|
}
|
|
.container {
|
padding-bottom: 20rpx;
|
}
|
|
.setting-area,
|
.print-area {
|
padding: 20rpx;
|
background-color: #fff;
|
margin: 10rpx;
|
border-radius: 10rpx;
|
}
|
|
.input-item {
|
display: flex;
|
align-items: center;
|
margin-bottom: 20rpx;
|
border-bottom: 1rpx solid #eee;
|
padding-bottom: 10rpx;
|
}
|
|
.input-item text {
|
width: 140rpx;
|
font-size: 28rpx;
|
color: #333;
|
}
|
|
.uni-easyinput {
|
flex: 1;
|
}
|
|
.btn {
|
width: 100%;
|
padding: 20rpx;
|
margin-bottom: 15rpx;
|
border-radius: 8rpx;
|
color: #fff;
|
font-size: 28rpx;
|
}
|
|
.primary {
|
background-color: #007AFF;
|
}
|
|
.danger {
|
background-color: #FF3B30;
|
}
|
|
.print {
|
background-color: #00C853;
|
}
|
|
.test {
|
background-color: #FF9500;
|
}
|
|
.print-title {
|
display: block;
|
font-size: 28rpx;
|
color: #666;
|
margin-bottom: 15rpx;
|
}
|
|
.status {
|
padding: 20rpx;
|
font-size: 28rpx;
|
color: #666;
|
}
|
|
.log {
|
margin: 10rpx;
|
padding: 20rpx;
|
background-color: #f5f5f5;
|
border-radius: 10rpx;
|
}
|
|
.log-title {
|
font-weight: bold;
|
display: block;
|
margin-bottom: 10rpx;
|
color: #333;
|
}
|
|
.log-item {
|
font-size: 26rpx;
|
color: #666;
|
margin-bottom: 5rpx;
|
word-break: break-all;
|
}
|
</style>
|