yyb
7 小时以前 52459bcaba196c47a0f28079729b48ce012eaef1
src/views/salesManagement/salesLedger/index.vue
@@ -63,8 +63,10 @@
                     style="width: 140px">
            <el-option label="未入库"
                       :value="0" />
            <el-option label="已入库"
            <el-option label="部分入库"
                       :value="1" />
            <el-option label="已入库"
                       :value="2" />
          </el-select>
        </el-form-item>
        <el-form-item>
@@ -214,6 +216,20 @@
                          type="danger">不足</el-tag>
                </template>
              </el-table-column>
              <el-table-column label="入库状态"
                               width="100px"
                               align="center">
                <template #default="scope">
                  <el-tag v-if="scope.row.productStockStatus == 1"
                  type="warning">部分入库</el-tag>
                  <el-tag v-else-if="scope.row.productStockStatus == 2"
                          type="success">已入库</el-tag>
                  <el-tag v-else-if="scope.row.productStockStatus == 0"
                          type="info">未出库</el-tag>
                  <el-tag v-else
                          type="danger">不足</el-tag>
                </template>
              </el-table-column>
              <!-- <el-table-column label="发货状态" width="140" align="center">
                        <template #default="scope">
                           <el-tag :type="getShippingStatusType(scope.row)" size="small">
@@ -328,6 +344,8 @@
            <el-tag v-if="Number(scope.row.stockStatus) === 0"
                    type="info">未入库</el-tag>
            <el-tag v-else-if="Number(scope.row.stockStatus) === 1"
                    type="success">部分入库</el-tag>
            <el-tag v-else-if="Number(scope.row.stockStatus) === 2"
                    type="success">已入库</el-tag>
            <el-tag v-else
                    type="info">-</el-tag>
@@ -353,9 +371,13 @@
                         prop="remarks"
                         width="200"
                         show-overflow-tooltip />
        <el-table-column label="客户备注"
                         prop="customerRemarks"
                         width="200"
                         show-overflow-tooltip />
        <el-table-column fixed="right"
                         label="操作"
                         width="200"
                         width="280"
                         align="center">
          <template #default="scope">
            <el-button link
@@ -369,6 +391,9 @@
            <el-button link
                       type="primary"
                       @click="downLoadFile(scope.row)">附件</el-button>
            <el-button link
                       type="primary"
                       @click="openLedgerQrDialog(scope.row)">二维码</el-button>
          </template>
        </el-table-column>
      </el-table>
@@ -441,9 +466,7 @@
                           :key="item.id"
                           :label="item.customerName"
                           :value="item.id">
                  {{
                    item.customerName + "——" + item.taxpayerIdentificationNumber
                  }}
                  {{ item.customerName + (item.taxpayerIdentificationNumber ? "——" + item.taxpayerIdentificationNumber : "") }}
                </el-option>
              </el-select>
            </el-form-item>
@@ -565,7 +588,7 @@
          </el-table-column>
          <el-table-column label="规格型号"
                           prop="specificationModel"
                           min-width="160">
                           min-width="200">
            <template #default="scope">
              <el-select v-if="scope.row.__editing"
                         v-model="scope.row.productModelId"
@@ -587,10 +610,11 @@
                           min-width="160">
            <template #default="scope">
              <el-input-number v-if="scope.row.__editing"
                               controls-position="right"
                               v-model="scope.row.thickness"
                               :min="0"
                               :step="0.000000000000001"
                               :precision="15"
                               :step="1"
                               :precision="2"
                               style="width: 100%"
                               placeholder="请输入"
                               clearable />
@@ -602,6 +626,7 @@
                           min-width="160">
            <template #default="scope">
              <el-input-number v-if="scope.row.__editing"
                               controls-position="right"
                               v-model="scope.row.width"
                               :min="0"
                               :step="1"
@@ -619,6 +644,7 @@
                           min-width="160">
            <template #default="scope">
              <el-input-number v-if="scope.row.__editing"
                               controls-position="right"
                               v-model="scope.row.height"
                               :min="0"
                               :step="1"
@@ -633,13 +659,14 @@
          </el-table-column>
          <el-table-column label="结算单片面积(㎡)"
                           prop="settlePieceArea"
                           min-width="160">
                           min-width="200">
            <template #default="scope">
              <el-input-number v-if="scope.row.__editing"
                               controls-position="right"
                               v-model="scope.row.settlePieceArea"
                               :min="0"
                               :step="0.00001"
                               :precision="5"
                               :step="1"
                               :precision="10"
                               style="width: 100%"
                               placeholder="请输入"
                               clearable
@@ -652,10 +679,11 @@
                           min-width="150">
            <template #default="scope">
              <el-input-number v-if="scope.row.__editing"
                               controls-position="right"
                               v-model="scope.row.quantity"
                               :step="0.1"
                               :step="1"
                               :min="0"
                               :precision="2"
                               :precision="0"
                               style="width: 100%"
                               placeholder="请输入"
                               clearable
@@ -666,13 +694,14 @@
          </el-table-column>
          <el-table-column label="面积(m²)"
                           prop="actualTotalArea"
                           min-width="160">
                           min-width="200">
            <template #default="scope">
              <el-input-number v-if="scope.row.__editing"
                               controls-position="right"
                               v-model="scope.row.actualTotalArea"
                               :min="0"
                               :step="0.00001"
                               :precision="5"
                               :step="1"
                               :precision="10"
                               style="width: 100%"
                               placeholder="自动计算" />
              <span v-else>{{ scope.row.actualTotalArea ?? "" }}</span>
@@ -680,7 +709,7 @@
          </el-table-column>
          <el-table-column label="含税单价(元)"
                           prop="taxInclusiveUnitPrice"
                           min-width="140">
                           min-width="160">
            <template #default="scope">
              <el-input-number v-if="scope.row.__editing"
                               :step="0.01"
@@ -772,7 +801,7 @@
          </el-table-column>
          <el-table-column label="楼层编号"
                           prop="floorCode"
                           min-width="140"
                           min-width="250"
                           show-overflow-tooltip>
            <template #default="scope">
              <el-input v-if="scope.row.__editing"
@@ -1505,9 +1534,10 @@
    <FormDialog v-model="importUpload.open"
                :title="importUpload.title"
                :width="'600px'"
                @close="importUpload.open = false"
                :loading="importUpload.isUploading"
                @close="onClose"
                @confirm="submitImportFile"
                @cancel="importUpload.open = false">
                @cancel="onClose">
      <el-upload ref="importUploadRef"
                 :limit="1"
                 accept=".xlsx,.xls"
@@ -1518,6 +1548,7 @@
                 :on-error="importUpload.onError"
                 :on-progress="importUpload.onProgress"
                 :on-change="importUpload.onChange"
                 :on-exceed="importUpload.onExceed"
                 :auto-upload="false"
                 drag>
        <i class="el-icon-upload"></i>
@@ -1609,6 +1640,69 @@
        </div>
      </template>
    </el-dialog>
    <!-- 入库产品选择弹窗 -->
    <el-dialog v-model="stockDialogVisible"
               title="选择入库产品"
               width="60%"
               :close-on-click-modal="false">
      <el-table :data="stockProductList"
                border
                stripe
                v-loading="stockLoading"
                height="400px"
                @selection-change="val => selectedStockProductIds = val.map(item => item.id)">
        <el-table-column type="selection"
                         width="55"
                         align="center" />
        <el-table-column align="center"
                         label="序号"
                         type="index"
                         width="60" />
        <el-table-column prop="productCategory"
                         label="产品大类"
                         show-overflow-tooltip />
        <el-table-column prop="specificationModel"
                         label="规格型号"
                         show-overflow-tooltip />
        <el-table-column prop="quantity"
                         label="数量"
                         width="100" />
        <el-table-column prop="stockedQuantity"
                         label="已入库数量"
                         width="120"
                         align="center"
                         show-overflow-tooltip />
        <el-table-column prop="floorCode"
                         label="楼层编号"
                         show-overflow-tooltip />
      </el-table>
      <template #footer>
        <el-button @click="stockDialogVisible = false">取消</el-button>
        <el-button type="primary"
                   @click="submitStock"
                   :disabled="selectedStockProductIds.length === 0">
          确认入库
        </el-button>
      </template>
    </el-dialog>
    <el-dialog v-model="ledgerQrDialogVisible"
               title="销售订单二维码"
               width="360px"
               draggable
               :close-on-click-modal="false">
      <div class="ledger-qr-dialog">
        <img v-if="ledgerQrCompositeUrl"
             :src="ledgerQrCompositeUrl"
             alt="销售订单二维码"
             class="ledger-qr-composite-img" />
        <el-button type="primary"
                   class="ledger-qr-save-btn"
                   :disabled="!ledgerQrCompositeUrl"
                   @click="downloadLedgerQrCode">
          保存图片
        </el-button>
      </div>
    </el-dialog>
  </div>
</template>
@@ -1656,6 +1750,7 @@
  import { printSalesOrder } from "./components/salesOrderPrint.js";
  import { printSalesDeliveryNote } from "./components/salesDeliveryPrint.js";
  import { printSalesLabel } from "./components/salesLabelPrint.js";
  import QRCode from "qrcode";
  // import { salesLedgerProductSetProcessFlowConfig } from "@/api/salesManagement/salesProcessFlowConfig.js";
  const userStore = useUserStore();
@@ -1684,6 +1779,116 @@
  const processFlowSelectBoundRouteId = ref(null);
  const processFlowSelectBoundRouteName = ref("");
  // 入库弹窗相关
  const stockDialogVisible = ref(false);
  const stockProductList = ref([]);
  const selectedStockProductIds = ref([]);
  const stockLoading = ref(false);
  const currentStockLedgerId = ref(null);
  const ledgerQrDialogVisible = ref(false);
  const ledgerQrCompositeUrl = ref("");
  const ledgerQrDownloadBaseName = ref("");
  const sanitizeLedgerQrFilename = s =>
    String(s)
      .replace(/[\\/:*?"<>|]/g, "_")
      .trim()
      .slice(0, 80) || "ledger";
  const wrapLedgerQrTextLines = (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 buildLedgerQrCompositeDataUrl = row =>
    new Promise((resolve, reject) => {
      const payload = JSON.stringify({ id: row.id });
      QRCode.toDataURL(payload, { width: 220, margin: 2 })
        .then(qrDataUrl => {
          const contract = (row.salesContractNo ?? "").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 = wrapLedgerQrTextLines(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 = sanitizeLedgerQrFilename(
              contract !== "—" ? contract : String(row.id)
            );
            resolve({ dataUrl: canvas.toDataURL("image/png"), baseName });
          };
          img.onerror = () => reject(new Error("二维码图片加载失败"));
          img.src = qrDataUrl;
        })
        .catch(reject);
    });
  const openLedgerQrDialog = async row => {
    if (row?.id === undefined || row?.id === null || row?.id === "") {
      ElMessage.warning("无法生成二维码:缺少台账 ID");
      return;
    }
    ledgerQrCompositeUrl.value = "";
    ledgerQrDownloadBaseName.value = "";
    try {
      const { dataUrl, baseName } = await buildLedgerQrCompositeDataUrl(row);
      ledgerQrCompositeUrl.value = dataUrl;
      ledgerQrDownloadBaseName.value = baseName;
      ledgerQrDialogVisible.value = true;
    } catch {
      ElMessage.error("二维码生成失败");
    }
  };
  const downloadLedgerQrCode = () => {
    if (!ledgerQrCompositeUrl.value) return;
    const a = document.createElement("a");
    a.href = ledgerQrCompositeUrl.value;
    a.download = `销售销售订单二维码-${ledgerQrDownloadBaseName.value}.png`;
    a.click();
  };
  // 用户信息表单弹框数据
  const operationType = ref("");
  const dialogFormVisible = ref(false);
@@ -1696,7 +1901,7 @@
      entryDateStart: undefined,
      entryDateEnd: undefined,
      deliveryStatus: undefined, // 发货状态:1未发货 2审批中 3审批失败 4已发货
      stockStatus: undefined, // 入库状态:0未入库 1已入库
      stockStatus: undefined, // 入库状态:0未入库 1部分入库 2已入库
    },
    form: {
      salesContractNo: "",
@@ -1952,6 +2157,30 @@
    if (!row) return false;
    if (!row.productCategory) {
      proxy.$modal.msgWarning("请选择产品大类");
      return false;
    }
    if (row.width <= 0) {
      proxy.$modal.msgWarning("宽必须大于0");
      return false;
    }
    if (row.height <= 0) {
      proxy.$modal.msgWarning("高必须大于0");
      return false;
    }
    if (row.settlePieceArea <= 0) {
      proxy.$modal.msgWarning("结算单片面积必须大于0");
      return false;
    }
    if (row.quantity <= 0) {
      proxy.$modal.msgWarning("数量必须大于0");
      return false;
    }
    if (row.actualTotalArea <= 0) {
      proxy.$modal.msgWarning("面积必须大于0");
      return false;
    }
    if (row.taxInclusiveUnitPrice <= 0) {
      proxy.$modal.msgWarning("含税单价必须大于0");
      return false;
    }
    if (!row.productModelId) {
@@ -2485,6 +2714,13 @@
    onChange: (file, fileList) => {
      console.log("文件状态改变", file, fileList);
    },
    onExceed: (files, fileList) => {
      if (importUploadRef.value) {
        importUploadRef.value.clearFiles();
        const file = files[0];
        importUploadRef.value.handleStart(file);
      }
    },
    onProgress: (event, file, fileList) => {
      console.log("上传中...", event.percent);
    },
@@ -2586,12 +2822,37 @@
      ElMessage.warning("所选数据缺少id,无法入库");
      return;
    }
    if (Number(row.stockStatus) === 1) {
      ElMessage.info("该台账已入库,无需重复操作");
    if (Number(row.stockStatus) === 2) {
      ElMessage.info("该台账已全部入库,无需重复操作");
      return;
    }
    currentStockLedgerId.value = id;
    selectedStockProductIds.value = [];
    stockProductList.value = [];
    stockDialogVisible.value = true;
    stockLoading.value = true;
    try {
      await ElMessageBox.confirm("确认对所选台账执行入库?", "提示", {
      const res = await productList({ salesLedgerId: id, type: 1 });
      stockProductList.value = [];
      stockProductList.value =
        res.data.filter(item => item.productStockStatus == 0) || [];
    } catch (e) {
      proxy?.$modal?.msgError?.("获取产品列表失败");
    } finally {
      stockLoading.value = false;
    }
  };
  const submitStock = async () => {
    if (selectedStockProductIds.value.length === 0) {
      ElMessage.warning("请选择至少一个产品进行入库");
      return;
    }
    try {
      await ElMessageBox.confirm("确认对所选产品执行入库?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
@@ -2599,10 +2860,15 @@
    } catch {
      return;
    }
    proxy?.$modal?.loading?.("正在入库,请稍候...");
    try {
      await salesStock({ id });
      await salesStock({
        salesLedgerId: currentStockLedgerId.value,
        salesLedgerProducts: selectedStockProductIds.value,
      });
      proxy?.$modal?.msgSuccess?.("入库成功");
      stockDialogVisible.value = false;
      await getList();
    } catch (e) {
      proxy?.$modal?.msgError?.("入库失败,请稍后重试");
@@ -3338,6 +3604,12 @@
  const downloadTemplate = () => {
    proxy.download("/sales/ledger/exportTemplate", {}, "销售台账导入模板.xlsx");
  };
  const onClose = () => {
    importUpload.open = false;
    if (importUploadRef.value) {
      importUploadRef.value.clearFiles();
    }
  };
  // 提交导入文件
  const submitImportFile = () => {
@@ -3504,7 +3776,28 @@
      } else {
        const res = await getProcessCard(selectedId);
        const processCardData = res?.data ?? {};
        printFinishedProcessCard(processCardData);
        const routeNodes = processCardData?.routeNodes;
        const isProcessRouteEmpty =
          !Array.isArray(routeNodes) || routeNodes.length === 0;
        if (isProcessRouteEmpty) {
          proxy.$modal.closeLoading();
          try {
            await ElMessageBox.confirm(
              "当前订单未绑定工艺路线也没有设置默认的工艺路线,是否仍要打印?",
              "提示",
              {
                confirmButtonText: "打印",
                cancelButtonText: "取消",
                type: "warning",
              }
            );
          } catch {
            return;
          }
          printFinishedProcessCard(processCardData);
        } else {
          printFinishedProcessCard(processCardData);
        }
      }
    } catch (error) {
      console.error(
@@ -3927,12 +4220,21 @@
    }
    // 只允许【未发货/审批失败】进入发货流程
    const canDeliveryLedgers = selectedRows.value.filter(r => {
      const status = Number(r.deliveryStatus);
      return status === 1 || status === 3;
    const statusItem = selectedRows.value[0].deliveryStatus;
    const isTrue = true;
    selectedRows.value.forEach(row => {
      if (row.deliveryStatus != 1 && row.deliveryStatus != 3) {
        proxy.$modal.msgWarning("仅未发货或审批失败的台账可以发货");
        isTrue = false;
        return;
      }
      if (row.deliveryStatus !== statusItem) {
        proxy.$modal.msgWarning("请选择相同状态的销售台账");
        isTrue = false;
        return;
      }
    });
    if (canDeliveryLedgers.length === 0) {
      proxy.$modal.msgWarning("仅未发货或审批失败的台账可以发货");
    if (!isTrue) {
      return;
    }
@@ -4207,4 +4509,20 @@
    justify-content: space-between;
    margin-bottom: 10px;
  }
  .ledger-qr-dialog {
    text-align: center;
    padding-bottom: 8px;
  }
  .ledger-qr-composite-img {
    max-width: 100%;
    height: auto;
    display: block;
    margin: 0 auto 28px;
  }
  .ledger-qr-save-btn {
    margin-bottom: 12px;
  }
</style>