liu
3 天以前 ee7ac8df8bce38add1f75baf876b49de94b9e00a
src/views/mes/pro/components/qr-trace-modal.vue
@@ -11,7 +11,7 @@
  import { Button, Empty, Input, message, Spin, Tag } from "ant-design-vue";
  import { getBagCodeByCode } from "#/api/mes/pro/bagCode";
  import { getBagCodeByCode, resolveTraceType } from "#/api/mes/pro/bagCode";
  import { getBatchByCode } from "#/api/mes/pro/batch";
  import { getPalletByCode } from "#/api/mes/pro/pallet";
  import { getQrConfig } from "#/api/mes/trace/config";
@@ -52,9 +52,10 @@
  /** 二维码展示内容:配置了溯源地址则拼 H5 URL,否则回退纯码值(与后端 buildQrCodeContent 一致) */
  const qrContent = computed(() => {
    if (!currentCode.value) return "";
    const code = bagTrace.value?.code || currentCode.value;
    return traceBaseUrl.value
      ? `${traceBaseUrl.value}?code=${encodeURIComponent(currentCode.value)}`
      : currentCode.value;
      ? `${traceBaseUrl.value}?code=${encodeURIComponent(code)}`
      : code;
  });
  /** 从完整 H5 链接中提取追溯码(兼容扫码枪扫描打印标签链接) */
@@ -70,24 +71,6 @@
      }
    }
    return trimmed;
  }
  /** 识别二维码内容类型:批次号(含临时批次码)/ 托盘码(含临时托盘码)/ 袋码 */
  function resolveCodeType(raw: string): TraceType | undefined {
    const value = raw.trim().toUpperCase();
    if (/^PB-\d{8}$/.test(value)) {
      return "batch";
    }
    if (/^PL-\d{8}$/.test(value)) {
      return "pallet";
    }
    if (/^.*-P\d{2}$/.test(value)) {
      return "pallet";
    }
    if (/^BAG-\d{8}$/.test(value) || /^.*-\d{4}$/.test(value)) {
      return "bag";
    }
    return "batch";
  }
  function resetState() {
@@ -113,11 +96,6 @@
      message.warning("二维码内容过长,请检查是否扫描正确");
      return;
    }
    const type = resolveCodeType(value);
    if (!type) {
      message.warning("无法识别二维码类型");
      return;
    }
    loading.value = true;
    searched.value = true;
    searchError.value = false;
@@ -128,15 +106,24 @@
    palletTrace.value = undefined;
    batchTrace.value = undefined;
    try {
      if (type === "batch") {
        batchTrace.value = await getBatchByCode(value);
        traceTypeLabel.value = "批次号";
      // 编码已统一为「16 位纯数字」,无法再按前缀判断类型,改由后端按编码解析类型
      const type = await resolveTraceType(value);
      if (type === "bag") {
        bagTrace.value = await getBagCodeByCode(value);
        currentCode.value = bagTrace.value.code || value;
        traceTypeLabel.value = "袋码";
      } else if (type === "pallet") {
        palletTrace.value = await getPalletByCode(value);
        currentCode.value = palletTrace.value.code || palletTrace.value.tempCode || value;
        traceTypeLabel.value = "托盘码";
      } else if (type === "batch") {
        batchTrace.value = await getBatchByCode(value);
        currentCode.value = batchTrace.value.code || batchTrace.value.tempCode || value;
        traceTypeLabel.value = "批次号";
      } else {
        bagTrace.value = await getBagCodeByCode(value);
        traceTypeLabel.value = "袋码";
        searchError.value = true;
        currentCode.value = undefined;
        return;
      }
      traceType.value = type;
    } catch {
@@ -148,58 +135,6 @@
    }
  }
  /** 当前追溯数据的关键信息(下载/打印图片叠加文字用) */
  function buildLabelText(): { label: string; value: string }[] {
    if (traceType.value === "bag" && bagTrace.value) {
      const d = bagTrace.value;
      return [
        { label: "袋码", value: d.code || "" },
        { label: "袋序号", value: d.bagNo ? String(d.bagNo) : "" },
        { label: "批次号", value: d.batchCode || "" },
        { label: "品种", value: d.itemName || "" },
        { label: "规格", value: d.itemSpecification || "" },
        {
          label: "产线",
          value: [d.lineCode, d.lineName].filter(Boolean).join(" / "),
        },
        { label: "生产日期", value: d.produceDate || "" },
        { label: "责任人", value: d.ownerUserName || "" },
      ];
    }
    if (traceType.value === "pallet" && palletTrace.value) {
      const d = palletTrace.value;
      return [
        { label: "托盘码", value: d.code || d.tempCode || "" },
        { label: "托盘序号", value: d.palletNo ? String(d.palletNo) : "" },
        { label: "批次号", value: d.batchCode || "" },
        { label: "品种", value: d.itemName || "" },
        { label: "规格", value: d.itemSpecification || "" },
        {
          label: "产线",
          value: [d.lineCode, d.lineName].filter(Boolean).join(" / "),
        },
        { label: "生产日期", value: d.produceDate || "" },
        { label: "责任人", value: d.ownerUserName || "" },
      ];
    }
    if (batchTrace.value) {
      const d = batchTrace.value;
      return [
        { label: "批次号", value: d.code || d.tempCode || "" },
        { label: "品种", value: d.itemName || "" },
        { label: "规格", value: d.itemSpecification || "" },
        {
          label: "产线",
          value: [d.lineCode, d.lineName].filter(Boolean).join(" / "),
        },
        { label: "生产日期", value: d.produceDate || "" },
        { label: "计划数量", value: d.planQuantity ? String(d.planQuantity) : "" },
        { label: "责任人", value: d.ownerUserName || "" },
      ];
    }
    return [];
  }
  function escapeHtml(value: string) {
    return value
      .replaceAll("&", "&")
@@ -207,14 +142,14 @@
      .replaceAll(">", ">");
  }
  /** 下载二维码 PNG(二维码 + 关键信息文字合成) */
  /** 下载二维码 PNG(二维码在上,码值在下) */
  function handleDownload() {
    const base64 = barcodeRef.value?.getImageBase64();
    if (!base64) {
      message.warning("二维码生成失败,无法下载");
      return;
    }
    const items = buildLabelText();
    const code = currentCode.value || "";
    const canvas = document.createElement("canvas");
    const ctx = canvas.getContext("2d");
    if (!ctx) {
@@ -223,45 +158,23 @@
    }
    const img = new Image();
    img.onload = () => {
      const padding = 20;
      const infoX = img.width + padding * 2;
      const infoWidth = 320;
      canvas.width = infoX + infoWidth + padding;
      canvas.height = Math.max(img.height + padding * 2, 300);
      const padding = 16;
      const gap = 4;
      const textHeight = 28;
      canvas.width = img.width + padding * 2;
      canvas.height = img.height + padding * 2 + gap + textHeight;
      ctx.fillStyle = "#ffffff";
      ctx.fillRect(0, 0, canvas.width, canvas.height);
      ctx.drawImage(img, padding, padding);
      ctx.fillStyle = "#000000";
      let y = padding + 10;
      const lineHeight = 26;
      items.forEach((item) => {
        ctx.font = "bold 14px 'Microsoft YaHei', Arial, sans-serif";
        ctx.fillText(`${item.label}:`, infoX, y);
        y += 18;
        ctx.font = "14px 'Microsoft YaHei', Arial, sans-serif";
        const text = item.value || "-";
        const maxWidth = infoWidth - 10;
        if (ctx.measureText(text).width > maxWidth) {
          let currentText = text;
          while (currentText.length > 0) {
            let line = "";
            for (let i = 0; i < currentText.length; i++) {
              const testLine = line + currentText[i];
              if (ctx.measureText(testLine).width > maxWidth) {
                break;
              }
              line = testLine;
            }
            ctx.fillText(line, infoX + 5, y);
            y += 16;
            currentText = currentText.slice(line.length);
          }
          y += 6;
        } else {
          ctx.fillText(text, infoX + 5, y);
          y += lineHeight;
        }
      });
      let fontSize = 20;
      ctx.font = `bold ${fontSize}px Arial, sans-serif`;
      while (ctx.measureText(code).width > img.width && fontSize > 1) {
        ctx.font = `bold ${--fontSize}px Arial, sans-serif`;
      }
      ctx.textAlign = "center";
      ctx.textBaseline = "middle";
      ctx.fillText(code, canvas.width / 2, padding + img.height + gap + textHeight / 2);
      const link = document.createElement("a");
      link.href = canvas.toDataURL("image/png");
      link.download = `${currentCode.value || "qr"}_${Date.now()}.png`;
@@ -278,7 +191,7 @@
      message.warning("二维码生成失败,无法打印");
      return;
    }
    const items = buildLabelText();
    const code = currentCode.value || "";
    const printWindow = window.open("", "_blank");
    if (!printWindow) {
      message.error("无法打开打印窗口,请检查浏览器设置");
@@ -286,12 +199,19 @@
    }
    printWindow.document.write(`<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title>打印二维码</title>
<style>*{margin:0;padding:0}body{font-family:'Microsoft YaHei',Arial,sans-serif;padding:20px}.print-container{text-align:center}.qr-img{max-width:100%;margin:20px 0}.info{margin-top:20px;text-align:left;font-size:13px}.info p{margin:4px 0}@media print{body{padding:0}.print-container{padding:20px}}</style>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:Arial,sans-serif;background:#fff;color:#000}
.print-container{width:40mm;padding:2mm;break-inside:avoid;text-align:center}
.qr-img{display:block;width:36mm;height:36mm;object-fit:contain}
.code{display:block;width:36mm;height:5mm;margin-top:0.5mm}
@page{margin:0}
</style>
</head><body><div class="print-container">
<img src="${base64}" class="qr-img" alt="二维码" />
<div class="info">
${items.map((item) => `<p><strong>${escapeHtml(item.label)}:</strong> ${escapeHtml(item.value || "-")}</p>`).join("")}
</div></div></body></html>`);
<svg class="code" viewBox="0 0 200 28" xmlns="http://www.w3.org/2000/svg" aria-label="码值">
<text x="100" y="20" text-anchor="middle" font-family="Arial,sans-serif" font-size="20" font-weight="700"${code.length > 16 ? ' textLength="196" lengthAdjust="spacingAndGlyphs"' : ''}>${escapeHtml(code)}</text>
</svg></div></body></html>`);
    printWindow.document.close();
    printWindow.addEventListener("load", () => {
      setTimeout(() => printWindow.print(), 500);
@@ -319,9 +239,10 @@
    schema: [
      {
        field: "code",
        label: "袋码(二维码内容)",
        label: "追溯码",
        span: 2,
      },
      { field: 'businessCode', label: '业务码(出入库)', span: 2 },
      {
        field: "bagNo",
        label: "袋序号",