src/views/procurementManagement/procurementLedger/index.vue
@@ -33,6 +33,20 @@
                      prefix-icon="Search"
                      @change="handleQuery" />
          </el-form-item>
          <el-form-item label="入库状态:">
            <el-select v-model="searchForm.stockStatus"
                       placeholder="请选择"
                       clearable
                       style="width: 140px"
                       @change="handleQuery">
              <el-option label="未入库"
                         :value="0" />
              <el-option label="部分入库"
                         :value="1" />
              <el-option label="已入库"
                         :value="2" />
            </el-select>
          </el-form-item>
          <el-form-item label="录入日期:">
            <el-date-picker v-model="searchForm.entryDate"
                            value-format="YYYY-MM-DD"
@@ -96,6 +110,16 @@
                               prop="availableQuality" />
              <el-table-column label="退货数量"
                               prop="returnQuality" />
              <el-table-column label="入库状态"
                               width="100px"
                               align="center">
                <template #default="scope">
                  <el-tag :type="getProductStockStatusType(scope.row.productStockStatus)"
                          size="small">
                    {{ stockStatusText[scope.row.productStockStatus] || '未入库' }}
                  </el-tag>
                </template>
              </el-table-column>
              <el-table-column label="税率(%)"
                               prop="taxRate" />
              <el-table-column label="含税单价(元)"
@@ -154,6 +178,16 @@
                         width="200"
                         show-overflow-tooltip
                         :formatter="formattedNumber" />
        <el-table-column label="入库状态"
                         width="120"
                         align="center">
          <template #default="scope">
            <el-tag :type="getStockStatusType(scope.row.stockStatus)"
                    size="small">
              {{ stockStatusText[scope.row.stockStatus] || '未入库' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="录入人"
                         prop="recorderName"
                         width="120"
@@ -168,7 +202,7 @@
                         show-overflow-tooltip />
        <el-table-column fixed="right"
                         label="操作"
                         width="120"
                         width="200"
                         align="center">
          <template #default="scope">
            <el-button link
@@ -178,6 +212,9 @@
            <el-button link
                       type="primary"
                       @click="downLoadFile(scope.row)">附件</el-button>
            <el-button link
                       type="primary"
                       @click="openProcurementQrDialog(scope.row)">二维码</el-button>
          </template>
        </el-table-column>
      </el-table>
@@ -689,6 +726,24 @@
    <FileListDialog ref="fileListRef"
                    v-model="fileListDialogVisible"
                    title="附件列表" />
    <el-dialog v-model="procurementQrDialogVisible"
               title="采购台账二维码"
               width="360px"
               draggable
               :close-on-click-modal="false">
      <div class="procurement-qr-dialog">
        <img v-if="procurementQrCompositeUrl"
             :src="procurementQrCompositeUrl"
             alt="采购台账二维码"
             class="procurement-qr-composite-img" />
        <el-button type="primary"
                   class="procurement-qr-save-btn"
                   :disabled="!procurementQrCompositeUrl"
                   @click="downloadProcurementQrCode">
          保存图片
        </el-button>
      </div>
    </el-dialog>
  </div>
</template>
@@ -732,6 +787,7 @@
    delPurchaseTemplate,
  } from "@/api/procurementManagement/procurementLedger.js";
  import useFormData from "@/hooks/useFormData.js";
  import QRCode from "qrcode";
  const { proxy } = getCurrentInstance();
  const tableData = ref([]);
@@ -784,6 +840,33 @@
      4: "danger", // 审批失败 - 红色
    };
    return typeMap[status] || "";
  };
  // 入库状态显示文本
  const stockStatusText = {
    0: "未入库",
    1: "部分入库",
    2: "已入库",
  };
  // 获取主表入库状态标签类型
  const getStockStatusType = status => {
    const typeMap = {
      0: "info",
      1: "success",
      2: "success",
    };
    return typeMap[status] || "info";
  };
  // 获取产品入库状态标签类型
  const getProductStockStatusType = status => {
    const typeMap = {
      0: "info",
      1: "warning",
      2: "success",
    };
    return typeMap[status] || "info";
  };
  const templateName = ref("");
@@ -897,6 +980,7 @@
      purchaseContractNumber: "", // 采购合同编号
      salesContractNo: "", // 销售合同编号
      projectName: "", // 项目名称
      stockStatus: undefined, // 入库状态
      entryDate: null, // 录入日期
      entryDateStart: undefined,
      entryDateEnd: undefined,
@@ -1871,6 +1955,117 @@
    }
  };
  const procurementQrDialogVisible = ref(false);
  const procurementQrCompositeUrl = ref("");
  const procurementQrDownloadBaseName = ref("");
  const sanitizeProcurementQrFilename = s =>
    String(s)
      .replace(/[\\/:*?"<>|]/g, "_")
      .trim()
      .slice(0, 80) || "ledger";
  const wrapProcurementQrTextLines = (ctx, text, maxWidth) => {
    const chars = [...text];
    const lines = [];
    let line = "";
    for (const ch of chars) {
      const test = line + ch;
      if (ctx.measureText(test).width > maxWidth && line.length) {
        lines.push(line);
        line = ch;
      } else {
        line = test;
      }
    }
    if (line) lines.push(line);
    return lines;
  };
  const buildProcurementQrCompositeDataUrl = row =>
    new Promise((resolve, reject) => {
      const payload = JSON.stringify({
        id: row.id,
        purchaseContractNumber: (row.purchaseContractNumber ?? "").trim(),
        type: "CG",
      });
      QRCode.toDataURL(payload, { width: 220, margin: 2 })
        .then(qrDataUrl => {
          const contract = (row.purchaseContractNumber ?? "").trim() || "—";
          const img = new Image();
          img.onload = () => {
            const QR_SIZE = 220;
            const padTop = 16;
            const gapAfterQr = 14;
            const bottomPad = 48;
            const horizontalPad = 20;
            const lineHeight = 20;
            const fontSize = 14;
            const label = `采购合同号:${contract}`;
            const canvas = document.createElement("canvas");
            const ctx = canvas.getContext("2d");
            canvas.width = Math.max(QR_SIZE + horizontalPad * 2, 280);
            ctx.font = `${fontSize}px "Microsoft YaHei", "PingFang SC", sans-serif`;
            const lines = wrapProcurementQrTextLines(
              ctx,
              label,
              canvas.width - horizontalPad * 2
            );
            const textBlockHeight = lines.length * lineHeight;
            canvas.height = padTop + QR_SIZE + gapAfterQr + textBlockHeight + bottomPad;
            ctx.fillStyle = "#ffffff";
            ctx.fillRect(0, 0, canvas.width, canvas.height);
            const qrX = (canvas.width - QR_SIZE) / 2;
            ctx.drawImage(img, qrX, padTop, QR_SIZE, QR_SIZE);
            ctx.fillStyle = "#606266";
            ctx.font = `${fontSize}px "Microsoft YaHei", "PingFang SC", sans-serif`;
            ctx.textAlign = "center";
            ctx.textBaseline = "top";
            const textY0 = padTop + QR_SIZE + gapAfterQr;
            lines.forEach((ln, i) => {
              ctx.fillText(ln, canvas.width / 2, textY0 + i * lineHeight);
            });
            const baseName = sanitizeProcurementQrFilename(
              contract !== "—" ? contract : String(row.id)
            );
            resolve({ dataUrl: canvas.toDataURL("image/png"), baseName });
          };
          img.onerror = () => reject(new Error("二维码图片加载失败"));
          img.src = qrDataUrl;
        })
        .catch(reject);
    });
  const openProcurementQrDialog = async row => {
    if (row?.id === undefined || row?.id === null || row?.id === "") {
      ElMessage.warning("无法生成二维码:缺少台账 ID");
      return;
    }
    procurementQrCompositeUrl.value = "";
    procurementQrDownloadBaseName.value = "";
    try {
      const { dataUrl, baseName } = await buildProcurementQrCompositeDataUrl(row);
      procurementQrCompositeUrl.value = dataUrl;
      procurementQrDownloadBaseName.value = baseName;
      procurementQrDialogVisible.value = true;
    } catch {
      ElMessage.error("二维码生成失败");
    }
  };
  const downloadProcurementQrCode = () => {
    if (!procurementQrCompositeUrl.value) return;
    const a = document.createElement("a");
    a.href = procurementQrCompositeUrl.value;
    a.download = `采购台账二维码-${procurementQrDownloadBaseName.value}.png`;
    a.click();
  };
  // 获取模板信息
  const getTemplateList = async () => {
    let res = await getPurchaseTemplateList();
@@ -2009,4 +2204,20 @@
      transform: scale(1.2);
    }
  }
  .procurement-qr-dialog {
    text-align: center;
    padding-bottom: 8px;
  }
  .procurement-qr-composite-img {
    max-width: 100%;
    height: auto;
    display: block;
    margin: 0 auto 28px;
  }
  .procurement-qr-save-btn {
    margin-bottom: 12px;
  }
</style>