liu
3 天以前 ee7ac8df8bce38add1f75baf876b49de94b9e00a
src/views/mes/pro/components/qr-trace-modal.vue
@@ -3,7 +3,7 @@
  import type { MesProBatchApi } from "#/api/mes/pro/batch";
  import type { MesProPalletApi } from "#/api/mes/pro/pallet";
  import { ref } from "vue";
  import { computed, ref } from "vue";
  import { Barcode, useVbenModal } from "@vben/common-ui";
  import { DICT_TYPE } from "@vben/constants";
@@ -11,9 +11,10 @@
  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";
  import { useDescription } from "#/components/description";
  defineOptions({ name: "MesProQrTraceModal" });
@@ -35,16 +36,41 @@
  const traceTypeLabel = ref("");
  /** 识别二维码内容类型:批次号 / 托盘码 / 袋码 */
  function resolveCodeType(raw: string): TraceType | undefined {
    const value = raw.trim().toUpperCase();
    if (/^.*-P\d{2}$/.test(value)) {
      return "pallet";
  /** 溯源 H5 页面基础地址(会话内只拉取一次) */
  const traceBaseUrl = ref<string>("");
  async function ensureTraceBaseUrl() {
    if (traceBaseUrl.value) return;
    try {
      const res = await getQrConfig();
      traceBaseUrl.value = res.traceBaseUrl || "";
    } catch {
      traceBaseUrl.value = "";
    }
    if (/^.*-\d{4}$/.test(value)) {
      return "bag";
  }
  /** 二维码展示内容:配置了溯源地址则拼 H5 URL,否则回退纯码值(与后端 buildQrCodeContent 一致) */
  const qrContent = computed(() => {
    if (!currentCode.value) return "";
    const code = bagTrace.value?.code || currentCode.value;
    return traceBaseUrl.value
      ? `${traceBaseUrl.value}?code=${encodeURIComponent(code)}`
      : code;
  });
  /** 从完整 H5 链接中提取追溯码(兼容扫码枪扫描打印标签链接) */
  function extractCodeFromUrl(raw: string): string {
    const trimmed = raw.trim();
    if (/^https?:\/\//i.test(trimmed)) {
      try {
        const url = new URL(trimmed);
        const code = url.searchParams.get("code");
        if (code) return code;
      } catch {
        // 解析失败按原始输入处理
      }
    }
    return "batch";
    return trimmed;
  }
  function resetState() {
@@ -60,18 +86,14 @@
  /** 扫码/输入二维码内容查询追溯信息 */
  async function handleSearch(raw?: string) {
    const value = (raw ?? codeInput.value)?.trim().toUpperCase();
    const input = (raw ?? codeInput.value)?.trim() ?? "";
    const value = extractCodeFromUrl(input).toUpperCase();
    if (!value) {
      message.warning("请扫描或输入二维码内容");
      return;
    }
    if (value.length > MAX_CODE_LENGTH) {
      message.warning("二维码内容过长,请检查是否扫描正确");
      return;
    }
    const type = resolveCodeType(value);
    if (!type) {
      message.warning("无法识别二维码类型");
      return;
    }
    loading.value = true;
@@ -84,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 {
@@ -104,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 || "" },
        { 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 || "" },
        { 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("&", "&amp;")
@@ -163,14 +142,14 @@
      .replaceAll(">", "&gt;");
  }
  /** 下载二维码 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) {
@@ -179,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`;
@@ -234,7 +191,7 @@
      message.warning("二维码生成失败,无法打印");
      return;
    }
    const items = buildLabelText();
    const code = currentCode.value || "";
    const printWindow = window.open("", "_blank");
    if (!printWindow) {
      message.error("无法打开打印窗口,请检查浏览器设置");
@@ -242,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);
@@ -259,6 +223,7 @@
      if (!isOpen) {
        return;
      }
      ensureTraceBaseUrl();
      const data = modalApi.getData<{ code?: string }>() || {};
      codeInput.value = data.code ?? undefined;
      resetState();
@@ -274,9 +239,10 @@
    schema: [
      {
        field: "code",
        label: "袋码(二维码内容)",
        label: "追溯码",
        span: 2,
      },
      { field: 'businessCode', label: '业务码(出入库)', span: 2 },
      {
        field: "bagNo",
        label: "袋序号",
@@ -357,7 +323,12 @@
    schema: [
      {
        field: "code",
        label: "托盘码(二维码内容)",
        label: "托盘码(正式托盘码)",
        span: 2,
      },
      {
        field: "tempCode",
        label: "临时托盘码(未绑定批次)",
        span: 2,
      },
      {
@@ -419,7 +390,12 @@
    schema: [
      {
        field: "code",
        label: "批次号(二维码内容)",
        label: "批次号(正式批次号)",
        span: 2,
      },
      {
        field: "tempCode",
        label: "临时批次码(待补录)",
        span: 2,
      },
      {
@@ -496,12 +472,28 @@
            class="mb-4 flex items-center gap-6 rounded bg-gray-100 p-5"
          >
            <div class="flex-shrink-0">
              <Barcode ref="barcodeRef" :content="currentCode" :width="200" />
              <Barcode ref="barcodeRef" :content="qrContent" :width="200" />
            </div>
            <div class="min-w-0 flex-1">
              <p class="text-lg font-bold break-all">{{ currentCode }}</p>
              <p class="mt-1 text-xs text-gray-500">
                码类型:{{ traceTypeLabel }}
                <Tag
                  v-if="traceType === 'batch' && batchTrace && !batchTrace.code"
                  color="warning"
                  class="ml-2"
                >
                  待补录
                </Tag>
                <Tag
                  v-else-if="
                    traceType === 'pallet' && palletTrace && !palletTrace.code
                  "
                  color="warning"
                  class="ml-2"
                >
                  未绑定批次
                </Tag>
              </p>
            </div>
          </div>