Houjie
3 天以前 51c7896fd8e45085dd5cdfff11e79a00ee0a7379
pages/user/location.vue
@@ -1,117 +1,656 @@
<template>
    <view>
      <cu-custom :bgColor="NavBarColor" :isBack="true">
   <view class="container">
      <cu-custom :bgColor="NavBarColor" :isBack="true" backRouterName="index">
         <block slot="backText">返回</block>
         <block slot="content">定位</block>
         <block slot="content">网络打印机配置</block>
      </cu-custom>
      <map
      style="width: 100%; height:500px;"
      :latitude="latitude"
      :longitude="longitude"
      :markers="marker"
      :scale="scale"
      >
      </map>
    </view>
      <!-- 打印机设置区域 -->
      <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 {
       data() {
           return {
            NavBarColor:this.NavBarColor,
               id:0, // 使用 marker点击事件 需要填写id
               title: 'map',
              latitude: 40.009704,  //纬度
            longitude: 116.374999,  //经度
           marker: [{
              id:0,
              latitude: 40.009704,//纬度
              longitude: 116.374999,//经度
              iconPath: '/static/location.png',    //显示的图标
              rotate:0,   // 旋转度数
              width:20,   //宽
              height:20,   //高
              title:'你在哪了',//标注点名
              alpha:0.5,   //透明度
              /* label:{//为标记点旁边增加标签   //因背景颜色H5不支持
                 content:'北京国炬公司',//文本
                color:'red',//文本颜色
                 fontSize:24,//文字大小
                  x:5,//label的坐标,原点是 marker 对应的经纬度
                  y:1,//label的坐标,原点是 marker 对应的经纬度
                  borderWidth:12,//边框宽度
                  borderColor:'pink',//边框颜色
                 borderRadius:20,//边框圆角
                 bgColor:'black',//背景色
                 padding:5,//文本边缘留白
                  textAlign:'right'//文本对齐方式。
            }, */
            callout:{//自定义标记点上方的气泡窗口 点击有效
              content:'北京国炬公司',//文本
              color:'#ffffff',//文字颜色
              fontSize:14,//文本大小
              borderRadius:2,//边框圆角
              bgColor:'#00c16f',//背景颜色
              display:'ALWAYS'//常显
            }
            // anchor:{//经纬度在标注图标的锚点,默认底边中点
            //     x:0,    原点为给出的经纬度
            //     y:0,
            // }
         }],
         scale:16,//地图缩放程度
         controls:[{//在地图上显示控件,控件不随着地图移动
          id:1,//控件id
          iconPath:'/static/login3.png',//显示的图标
            clickable:true,
          position:{//控件在地图的位置
            left:15,
            top:15,
            width:50,
            height:50
           },
        }],
         circles:[{//在地图上显示圆
          latitude: 40.009704,
          longitude: 116.374999,
          radius:50,//半径
            fillColor:"#ffffffAA",//填充颜色
          color:"#55aaffAA",//描边的颜色
          strokeWidth:1//描边的宽度
        }],
       /*  polyline:[{//指定一系列坐标点,从数组第一项连线至最后一项
          points:[
             {latitude: 40.009153,longitude: 116.374935},
             {latitude: 40.009704,longitude: 116.374999},
          ],
          color:"#0000AA",//线的颜色
          width:2,//线的宽度
          dottedLine:true,//是否虚线
          arrowLine:true,    //带箭头的线 开发者工具暂不支持该属性
        }], */
        }
       },
      onLoad() {
         this.getLocation()
      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'
         };
      },
       methods: {
         getLocation(){
            uni.getLocation({
                type: 'gcj02',
                success: function (res) {
                    console.log('当前位置的经度:' + res.longitude);
                    console.log('当前位置的纬度:' + res.latitude);
                },
               fail: function (res) {
                   console.log('当前位置的经度');
               }
      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>
</style>
<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>