From 52459bcaba196c47a0f28079729b48ce012eaef1 Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期六, 18 四月 2026 13:35:47 +0800
Subject: [PATCH] feat: 添加销售订单二维码功能及相关样式,更新操作列宽度

---
 src/views/salesManagement/salesLedger/index.vue |  352 +++++++++++++++++++++++++++++++++++++++++++++++++++++-----
 1 files changed, 321 insertions(+), 31 deletions(-)

diff --git a/src/views/salesManagement/salesLedger/index.vue b/src/views/salesManagement/salesLedger/index.vue
index 9455101..61251e2 100644
--- a/src/views/salesManagement/salesLedger/index.vue
+++ b/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>
@@ -359,7 +377,7 @@
                          show-overflow-tooltip />
         <el-table-column fixed="right"
                          label="鎿嶄綔"
-                         width="200"
+                         width="280"
                          align="center">
           <template #default="scope">
             <el-button link
@@ -373,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>
@@ -445,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>
@@ -569,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"
@@ -591,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 />
@@ -606,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"
@@ -623,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"
@@ -637,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
@@ -656,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
@@ -670,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>
@@ -684,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"
@@ -776,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"
@@ -1509,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"
@@ -1522,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>
@@ -1613,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>
 
@@ -1660,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();
@@ -1688,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);
@@ -1700,7 +1901,7 @@
       entryDateStart: undefined,
       entryDateEnd: undefined,
       deliveryStatus: undefined, // 鍙戣揣鐘舵�侊細1鏈彂璐� 2瀹℃壒涓� 3瀹℃壒澶辫触 4宸插彂璐�
-      stockStatus: undefined, // 鍏ュ簱鐘舵�侊細0鏈叆搴� 1宸插叆搴�
+      stockStatus: undefined, // 鍏ュ簱鐘舵�侊細0鏈叆搴� 1閮ㄥ垎鍏ュ簱 2宸插叆搴�
     },
     form: {
       salesContractNo: "",
@@ -2513,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);
     },
@@ -2614,12 +2822,37 @@
       ElMessage.warning("鎵�閫夋暟鎹己灏慽d锛屾棤娉曞叆搴�");
       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",
@@ -2627,10 +2860,15 @@
     } catch {
       return;
     }
+
     proxy?.$modal?.loading?.("姝e湪鍏ュ簱锛岃绋嶅��...");
     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?.("鍏ュ簱澶辫触锛岃绋嶅悗閲嶈瘯");
@@ -3366,6 +3604,12 @@
   const downloadTemplate = () => {
     proxy.download("/sales/ledger/exportTemplate", {}, "閿�鍞彴璐﹀鍏ユā鏉�.xlsx");
   };
+  const onClose = () => {
+    importUpload.open = false;
+    if (importUploadRef.value) {
+      importUploadRef.value.clearFiles();
+    }
+  };
 
   // 鎻愪氦瀵煎叆鏂囦欢
   const submitImportFile = () => {
@@ -3532,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(
@@ -3955,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;
     }
 
@@ -4235,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>

--
Gitblit v1.9.3