From 7501ef192740ad7f138d1dfb14924fe40aa3f49b Mon Sep 17 00:00:00 2001
From: 647556386 <647556386@qq.com>
Date: 星期一, 20 四月 2026 21:00:46 +0800
Subject: [PATCH] 1

---
 项目代码/WIDESEA_WMSClient/src/extension/outbound/extend/NoStockOut.vue |  608 +++++++++++++++++++++++++++++-------------------------
 1 files changed, 323 insertions(+), 285 deletions(-)

diff --git "a/\351\241\271\347\233\256\344\273\243\347\240\201/WIDESEA_WMSClient/src/extension/outbound/extend/NoStockOut.vue" "b/\351\241\271\347\233\256\344\273\243\347\240\201/WIDESEA_WMSClient/src/extension/outbound/extend/NoStockOut.vue"
index d058411..7fc2023 100644
--- "a/\351\241\271\347\233\256\344\273\243\347\240\201/WIDESEA_WMSClient/src/extension/outbound/extend/NoStockOut.vue"
+++ "b/\351\241\271\347\233\256\344\273\243\347\240\201/WIDESEA_WMSClient/src/extension/outbound/extend/NoStockOut.vue"
@@ -8,71 +8,71 @@
       title="铏氭嫙鍑哄叆搴�"
       class="custom-vol-box"
     >
+      <!-- 鎻愪氦閬僵灞傦細瑕嗙洊鏁翠釜寮圭獥鍐呭鍖哄煙 -->
+      <div class="submit-mask" v-show="submitLoading">
+        <div class="mask-content">
+          <el-loading-spinner class="loading-icon" />
+          <span class="loading-text">姝e湪鎻愪氦鍑哄簱锛岃绋嶅��...</span>
+        </div>
+      </div>
+
       <div>
-        <!-- 鍗曟嵁閫夋嫨鍖哄煙锛堝彲杈撳叆鎼滅储锛� -->
-        <el-form :inline="true" :model="orderForm" style="margin-bottom: 20px; align-items: flex-end;">
-          <el-form-item label="鍑哄簱鍗曟嵁:" name="outboundOrderId">
-            <el-select
-              v-model="orderForm.outboundOrderId"
-              placeholder="璇烽�夋嫨鎴栨壂鎻忓嚭搴撳崟鎹彿"
+        <!-- 鍗曟嵁杈撳叆鍖哄煙锛堟敮鎸佹壂鐮侊級 -->
+        <el-form 
+          :inline="true" 
+          :model="orderForm" 
+          style="margin-bottom: 20px; align-items: flex-end;"
+          @submit.prevent
+        >
+          <el-form-item label="鍑哄簱鍗曟嵁:" name="outboundOrderNo">
+            <el-input
+              v-model="orderForm.outboundOrderNo"
+              placeholder="璇疯緭鍏ユ垨鎵弿鍑哄簱鍗曟嵁鍙�"
               clearable
               style="width: 220px; margin-right: 10px;"
-              @change="handleOrderChange"
-              @visible-change="handleOutboundVisibleChange"
-              :loading="loading"
-              filterable
-              :filter-method="filterOutboundOrders"
-              @input="handleOutboundScanInput"  
-              ref="outboundSelectRef"        
-            >
-              <el-option
-                v-for="order in filteredOutboundOrders"
-                :key="order.id"
-                :label="order.orderNo"
-                :value="order.id"
-              ></el-option>
-            </el-select>
+              @input="handleOutboundInput"
+              @keyup.enter="validateOutboundOrder"
+              ref="outboundInputRef"
+              :disabled="loading || submitLoading || isOutboundVerified"
+            ></el-input>
+            <span v-if="isOutboundVerified" class="verified-tag">鉁� 宸查獙璇�</span>
+            <span v-else-if="loading" class="loading-tag">鉁� 楠岃瘉涓�...</span>
           </el-form-item>
-          <el-form-item label="閲囪喘鍗曟嵁:" name="purchaseOrderId">
-            <el-select
-              v-model="orderForm.purchaseOrderId"
-              placeholder="璇烽�夋嫨鎴栨壂鎻忛噰璐崟鎹彿"
+          <el-form-item label="閲囪喘鍗曟嵁:" name="purchaseOrderNo">
+            <el-input
+              v-model="orderForm.purchaseOrderNo"
+              placeholder="鎵爜鏉$爜鍚庤嚜鍔ㄥ~鍏�"
               clearable
               style="width: 220px; margin-right: 10px;"
-              @change="handleOrderChange"
-              @visible-change="handlePurchaseVisibleChange"
-              :loading="loading"
-              filterable
-              :filter-method="filterPurchaseOrders"
-              @input="handlePurchaseScanInput" 
-              ref="purchaseSelectRef"
-            >
-              <el-option
-                v-for="order in filteredPurchaseOrders"
-                :key="order.id"
-                :label="order.orderNo"
-                :value="order.id"
-              ></el-option>
-            </el-select>
+              @input="handlePurchaseInput"
+              readonly
+              ref="purchaseInputRef"
+              :disabled="submitLoading"
+            ></el-input>
           </el-form-item>
         </el-form>
 
         <!-- 涓婃柟杈撳叆妗� -->
-        <el-form :inline="true" :model="formData" ref="formRef" style="margin-bottom: 20px; align-items: flex-end;">
+        <el-form 
+          :inline="true" 
+          :model="formData" 
+          ref="formRef" 
+          style="margin-bottom: 20px; align-items: flex-end;"
+          @submit.prevent  
+        >
           <el-form-item
             label="鎵弿鏉$爜:"
             style="width: 80%"
             name="barcode"
-            :rules="[{ required: true, message: '璇锋壂鎻忔垨杈撳叆鏉$爜', trigger: 'blur' }]"
           >
             <el-input
               ref="barcodeInputRef"
               v-model="formData.barcode"
               placeholder="璇蜂娇鐢ㄦ壂鐮佹灙鎵弿鏉$爜锛屾垨鎵嬪姩杈撳叆"
-              @keyup.enter="handleScan"
+              @keydown.enter="debouncedHandleScan" 
               autofocus
               class="custom-input"
-              :disabled="!orderForm.outboundOrderId || !orderForm.purchaseOrderId"
+              :disabled="!isOutboundVerified || loading || submitLoading"
             ></el-input>
           </el-form-item>
           <el-form-item>
@@ -81,7 +81,7 @@
               size="small" 
               @click="handleScan" 
               class="custom-button"
-              :disabled="!orderForm.outboundOrderId || !orderForm.purchaseOrderId || loading"
+              :disabled="!isOutboundVerified || loading || submitLoading"
             >
               <Search /> 纭鎵弿
             </el-button>
@@ -97,19 +97,28 @@
             <div class="card-body">
               <el-scrollbar height="400px" class="custom-scrollbar">
                 <transition-group name="barcode-item-transition">
-                  <div class="barcode-item" v-for="(item, index) in scannedBarcodes" :key="item.barcode" :data-index="index">
-                    <span class="barcode-text">{{ index + 1 }}. {{ item.barcode }}</span>
+                  <div class="barcode-item" v-for="(item, index) in scannedBarcodes" :key="`${item.barcode}-${index}`">
+                    <div class="barcode-detail">
+                      <div class="detail-row"><span class="label">鏉$爜锛�</span><span class="value">{{ item.barcode || '-' }}</span></div>
+                      <div class="detail-row"><span class="label">鐗╂枡缂栫爜锛�</span><span class="value">{{ item.materielCode || '-' }}</span></div>
+                      <div class="detail-row"><span class="label">鐗╂枡鍚嶇О锛�</span><span class="value">{{ item.materielName || '-' }}</span></div>
+                      <div class="detail-row"><span class="label">鎵规鍙凤細</span><span class="value">{{ item.batchNo || '-' }}</span></div>
+                      <div class="detail-row"><span class="label">鏉$爜鏁伴噺锛�</span><span class="value">{{ item.orderQuantity || item.quantity || 0 }}</span></div>
+                      <div class="detail-row"><span class="label">渚涘簲鍟嗙紪鐮侊細</span><span class="value">{{ item.supplyCode || '-' }}</span></div>
+                      <div class="detail-row"><span class="label">閲囪喘鍗曞彿锛�</span><span class="value">{{ item.purchaseOrderNo || '-' }}</span></div>
+                    </div>
                     <el-button
                       class="delete-btn"
                       @click="removeItem(index, item.barcode)"
                       icon="Delete"
                       circle
-                      :disabled="loading"
+                      :disabled="loading || submitLoading"
                     ></el-button>
                   </div>
                 </transition-group>
                 <div class="empty-tip" v-if="scannedBarcodes.length === 0">
-                  <span>鏆傛棤鎵弿璁板綍锛岃鍏堥�夋嫨鍗曟嵁鍚庢壂鎻忔潯鐮�</span>
+                  <span v-if="isOutboundVerified">鏆傛棤鎵弿璁板綍锛岃鎵弿鏉$爜</span>
+                  <span v-else>璇峰厛杈撳叆骞堕獙璇佹湁鏁堢殑鍑哄簱鍗曟嵁鍙�</span>
                 </div>
               </el-scrollbar>
             </div>
@@ -123,12 +132,18 @@
             type="primary" 
             size="small" 
             @click="submit" 
-            :disabled="scannedBarcodes.length === 0 || !orderForm.outboundOrderId || !orderForm.purchaseOrderId || loading" 
+            :disabled="scannedBarcodes.length === 0 || !isOutboundVerified || loading || submitLoading" 
             class="submit-btn"
           >
             <Check /> 鎻愪氦鍑哄簱
           </el-button>
-          <el-button type="text" size="small" @click="showDetailBox = false" class="cancel-btn" :disabled="loading">
+          <el-button 
+            type="text" 
+            size="small" 
+            @click="handleCancel" 
+            class="cancel-btn" 
+            :disabled="loading || submitLoading"
+          >
             鍙栨秷
           </el-button>
         </div>
@@ -138,310 +153,279 @@
 </template>
 
 <script setup>
-import { ref, reactive, onMounted, nextTick } from 'vue';
+import { ref, reactive, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
 import { ElMessage } from 'element-plus';
-
+import { Search, Check } from '@element-plus/icons-vue';
 import VolBox from "@/components/basic/VolBox.vue";
 import http from '@/api/http';
 
 // 鍝嶅簲寮忔暟鎹�
 const showDetailBox = ref(false);
-const orderForm = reactive({
-  outboundOrderId: "",
-  purchaseOrderId: ""
-});
-const formData = reactive({
-  barcode: "",
-});
+const orderForm = reactive({ outboundOrderNo: "", purchaseOrderNo: "" });
+const formData = reactive({ barcode: "" });
 const scannedBarcodes = ref([]);
-const outboundOrders = ref([]);
-const purchaseOrders = ref([]);
-const filteredOutboundOrders = ref([]);
-const filteredPurchaseOrders = ref([]);
 const loading = ref(false);
+const submitLoading = ref(false);
+const isOutboundVerified = ref(false);
+let unlockCalled = false; // 闃叉閲嶅瑙i攣
 
 // 妯℃澘寮曠敤
 const formRef = ref(null);
 const barcodeInputRef = ref(null);
-const outboundSelectRef = ref(null); // 鏂板锛氬嚭搴撳崟select鐨剅ef
-const purchaseSelectRef = ref(null); // 鏂板锛氶噰璐崟select鐨剅ef
+const outboundInputRef = ref(null);
+const purchaseInputRef = ref(null);
 
-// 鐢ㄤ簬闃叉杈撳叆浜嬩欢鍜宑hange浜嬩欢鍐茬獊鐨勯攣
-const isProcessingScan = ref(false);
+// 闊抽璧勬簮
+const successAudioSrc = require('@/assets/audio/success.mp3');
+const errorAudioSrc = require('@/assets/audio/error.mp3');
+const playAudio = (src, volume = 0.8) => {
+  try { const audio = new Audio(src); audio.volume = volume; audio.play().catch(() => {}); } catch(e) {}
+};
+const playSuccess = () => playAudio(successAudioSrc);
+const playError = () => playAudio(errorAudioSrc);
 
-// 缁勪欢鎸傝浇鏃跺垵濮嬪寲杩囨护鍚庣殑鍒楄〃
-onMounted(() => {
-  filteredOutboundOrders.value = [...outboundOrders.value];
-  filteredPurchaseOrders.value = [...purchaseOrders.value];
+// ========== 瑙i攣鏍稿績閫昏緫锛堜娇鐢ㄦ柊鎺ュ彛 MovePickingOrders锛� ==========
+const unlockOutboundOrder = async () => {
+  if (!isOutboundVerified.value || !orderForm.outboundOrderNo?.trim()) return;
+  if (unlockCalled) return;
+  unlockCalled = true;
+  const outboundOrderNo = orderForm.outboundOrderNo.trim();
+  try {
+    // 鏇挎崲涓� MovePickingOrders 鎺ュ彛
+    await http.post(`/api/OutboundPicking/MovePickingOrders?outOrder=${outboundOrderNo}`, null, "瑙i攣鍑哄簱鍗曚腑...").catch(() => {});
+  } catch (error) {
+    // 闈欓粯澶辫触
+  }
+};
+
+// 椤甸潰鍏抽棴/鍒锋柊鏃剁殑鍚屾瑙i攣锛堜娇鐢� sendBeacon 鎴� fetch keepalive锛�
+const unlockOnPageUnload = () => {
+  if (!isOutboundVerified.value || !orderForm.outboundOrderNo?.trim()) return;
+  const outboundOrderNo = orderForm.outboundOrderNo.trim();
+  const url = `/api/OutboundPicking/MovePickingOrders?outOrder=${encodeURIComponent(outboundOrderNo)}`;
+  // 浣跨敤 fetch keepalive 鍙戦��
+  fetch(url, { method: 'POST', keepalive: true, headers: { 'Content-Type': 'application/json' } }).catch(() => {});
+};
+
+// 鐩戝惉寮圭獥鍏抽棴锛堜换浣曟柟寮忥細鍙夊彿銆丒SC銆侀伄缃┿�佸彇娑堟寜閽瓑锛�
+watch(showDetailBox, (newVal, oldVal) => {
+  if (oldVal === true && newVal === false) {
+    // 寮圭獥鍏抽棴鏃惰皟鐢ㄨВ閿�
+    unlockOutboundOrder();
+  }
 });
 
-// 鎵撳紑寮圭獥
+// 鍙栨秷鎸夐挳
+const handleCancel = (e) => {
+  e?.stopPropagation();
+  e?.preventDefault();
+  showDetailBox.value = false;
+};
+
+// 鎵撳紑寮圭獥锛堥噸缃墍鏈夌姸鎬侊級
 const open = () => {
+  unlockCalled = false;
   showDetailBox.value = true;
   scannedBarcodes.value = [];
   formData.barcode = "";
-  orderForm.outboundOrderId = "";
-  orderForm.purchaseOrderId = "";
-  nextTick(() => {
-    barcodeInputRef.value?.focus();
-  });
+  orderForm.outboundOrderNo = "";
+  orderForm.purchaseOrderNo = "";
+  submitLoading.value = false;
+  isOutboundVerified.value = false;
+  nextTick(() => outboundInputRef.value?.focus());
 };
 
-// 鍔犺浇鍑哄簱鍗曟嵁鍒楄〃
-const loadOutboundOrders = async () => {
-  if (outboundOrders.value.length > 0) return;
+// 楠岃瘉鍑哄簱鍗曞彿锛堜粛浣跨敤鍘熸帴鍙o紝姝ゆ帴鍙g敤浜庨獙璇佸崟鎹湁鏁堟�э級
+const validateOutboundOrder = async () => {
+  const outboundOrderNo = orderForm.outboundOrderNo.trim();
+  if (!outboundOrderNo) { ElMessage.warning("璇疯緭鍏ュ嚭搴撳崟鎹彿"); return; }
   try {
     loading.value = true;
-    const res = await http.get("/api/OutboundPicking/GetAvailablePickingOrders");
-    
-    if (res.code === 0) {
-      outboundOrders.value = res.data.map(orderNo => ({
-        id: orderNo,
-        orderNo: orderNo
-      }));
-      filteredOutboundOrders.value = [...outboundOrders.value];
-    } else {
-      ElMessage.error("鍔犺浇鍑哄簱鍗曟嵁澶辫触锛�" + (res.message || '鏈煡閿欒'));
+    const res = await http.post(`/api/OutboundPicking/GetAvailablePickingOrders?outOrder=${outboundOrderNo}`, "楠岃瘉鍑哄簱鍗曟嵁鍙蜂腑...");
+    if (res.status !== true) {
+      orderForm.outboundOrderNo = "";
+      isOutboundVerified.value = false;
+      ElMessage.error(res.message || "鍑哄簱鍗曟嵁鍙烽獙璇佸け璐�");
+      nextTick(() => outboundInputRef.value?.focus());
+      return;
     }
+    isOutboundVerified.value = true;
+    ElMessage.success("鍑哄簱鍗曟嵁鍙烽獙璇侀�氳繃");
+    nextTick(() => barcodeInputRef.value?.focus());
   } catch (error) {
-    ElMessage.error("鍔犺浇鍑哄簱鍗曟嵁寮傚父锛�" + error.message);
+    orderForm.outboundOrderNo = "";
+    isOutboundVerified.value = false;
+    ElMessage.error(`楠岃瘉寮傚父锛�${error.message || "缃戠粶閿欒"}`);
+    nextTick(() => outboundInputRef.value?.focus());
   } finally {
     loading.value = false;
   }
 };
 
-// 鍔犺浇閲囪喘鍗曟嵁鍒楄〃
-const loadPurchaseOrders = async () => {
-  if (purchaseOrders.value.length > 0) return;
-  try {
-    loading.value = true;
-    const res = await http.get("/api/OutboundPicking/GetAvailablePurchaseOrders");
-    
-    if (res.status === true) {
-      purchaseOrders.value = res.data.map(orderNo => ({
-        id: orderNo,
-        orderNo: orderNo
-      }));
-      filteredPurchaseOrders.value = [...purchaseOrders.value];
-    } else {
-      ElMessage.error("鍔犺浇閲囪喘鍗曟嵁澶辫触锛�" + (res.message || '鏈煡閿欒'));
-    }
-  } catch (error) {
-    ElMessage.error("鍔犺浇閲囪喘鍗曟嵁寮傚父锛�" + error.message);
-  } finally {
-    loading.value = false;
-  }
+const handleOutboundInput = (val) => { if (val?.trim()) isOutboundVerified.value = false; };
+const handlePurchaseInput = (val) => {};
+
+// 鑾峰彇閲囪喘鍗曞彿
+const getPurchaseOrderByBarcode = async (barcode) => {
+  const res = await http.post(`/api/OutboundPicking/GetPurchaseOrderByBarcode?barCode=${encodeURIComponent(barcode)}`, "鏌ヨ閲囪喘鍗曞彿涓�...");
+  if (res.status !== true) throw new Error(res.message || "鏌ヨ澶辫触");
+  let purchaseOrderNo = '';
+  if (Array.isArray(res.data) && res.data.length > 0) purchaseOrderNo = res.data[0].purchaseOrderNo || res.data[0].orderId;
+  else purchaseOrderNo = res.data?.purchaseOrderNo || res.data?.orderId;
+  return purchaseOrderNo;
 };
 
-// 鍑哄簱鍗曟嵁杩囨护鏂规硶
-const filterOutboundOrders = (value) => {
-  if (!value) {
-    filteredOutboundOrders.value = [...outboundOrders.value];
-  } else {
-    const lowerValue = value.toLowerCase();
-    filteredOutboundOrders.value = outboundOrders.value.filter(order => 
-      order.orderNo.toLowerCase().includes(lowerValue)
-    );
-  }
-};
-
-// 閲囪喘鍗曟嵁杩囨护鏂规硶
-const filterPurchaseOrders = (value) => {
-  if (!value) {
-    filteredPurchaseOrders.value = [...purchaseOrders.value];
-  } else {
-    const lowerValue = value.toLowerCase();
-    filteredPurchaseOrders.value = purchaseOrders.value.filter(order => 
-      order.orderNo.toLowerCase().includes(lowerValue)
-    );
-  }
-};
-
-// 鍑哄簱鍗曟嵁涓嬫媺妗嗘樉绀�/闅愯棌鏃惰Е鍙�
-const handleOutboundVisibleChange = (visible) => {
-  if (visible) {
-    loadOutboundOrders();
-  } else {
-    // 褰撲笅鎷夋鍏抽棴鏃讹紝濡傛灉鏄壂鎻忔搷浣滃鑷寸殑锛屾竻绌鸿緭鍏ユ
-    if (isProcessingScan.value) {
-        nextTick(() => {
-            outboundSelectRef.value?.clearInput();
-            isProcessingScan.value = false;
-        });
-    }
-  }
-};
-
-// 閲囪喘鍗曟嵁涓嬫媺妗嗘樉绀�/闅愯棌鏃惰Е鍙�
-const handlePurchaseVisibleChange = (visible) => {
-  if (visible) {
-    loadPurchaseOrders();
-  } else {
-    // 褰撲笅鎷夋鍏抽棴鏃讹紝濡傛灉鏄壂鎻忔搷浣滃鑷寸殑锛屾竻绌鸿緭鍏ユ
-    if (isProcessingScan.value) {
-        nextTick(() => {
-            purchaseSelectRef.value?.clearInput();
-            isProcessingScan.value = false;
-        });
-    }
-  }
-};
-
-// 鍗曟嵁閫夋嫨鍙樺寲鏃惰Е鍙�
-const handleOrderChange = () => {
-  // 濡傛灉鏄墜鍔ㄩ�夋嫨锛屽垯涓嶆竻绌鸿緭鍏ユ
-  isProcessingScan.value = false;
-  
-  scannedBarcodes.value = [];
-  nextTick(() => {
-    barcodeInputRef.value?.focus();
-  });
-};
-
-/**
- * 澶勭悊鍑哄簱鍗曟壂鎻忚緭鍏�
- * @param {string} scanText - 鎵弿鏋緭鍏ョ殑鏂囨湰
- */
-const handleOutboundScanInput = async (scanText) => {
-  // 閬垮厤澶勭悊绌哄瓧绗︿覆鎴栫敱change浜嬩欢瑙﹀彂鐨勫唴閮ㄦ搷浣�
-  if (!scanText || isProcessingScan.value) return;
-
-  // 鏌ユ壘瀹屽叏鍖归厤鐨勮鍗�
-  const matchedOrder = outboundOrders.value.find(order => order.orderNo === scanText);
-
-  if (matchedOrder) {
-    isProcessingScan.value = true; // 鏍囪涓烘鍦ㄥ鐞嗘壂鎻�
-    // 鎵嬪姩璧嬪��
-    orderForm.outboundOrderId = matchedOrder.id;
-    ElMessage.success(`鎴愬姛閫夋嫨鍑哄簱鍗曪細${matchedOrder.orderNo}`);
-    
-    // 寤惰繜鑱氱劍鍒颁笅涓�涓緭鍏ユ锛岀‘淇濊祴鍊煎畬鎴�
-    setTimeout(() => {
-        purchaseSelectRef.value?.focus();
-    }, 100);
-  }
-  // 濡傛灉娌℃湁鍖归厤椤癸紝涓嶅仛浠讳綍浜嬶紝璁〆l-select淇濇寔杩囨护鐘舵�侊紝鐢ㄦ埛鍙互鎵嬪姩閫夋嫨
-};
-
-/**
- * 澶勭悊閲囪喘鍗曟壂鎻忚緭鍏�
- * @param {string} scanText - 鎵弿鏋緭鍏ョ殑鏂囨湰
- */
-const handlePurchaseScanInput = async (scanText) => {
-  if (!scanText || isProcessingScan.value) return;
-
-  const matchedOrder = purchaseOrders.value.find(order => order.orderNo === scanText);
-
-  if (matchedOrder) {
-    isProcessingScan.value = true; // 鏍囪涓烘鍦ㄥ鐞嗘壂鎻�
-    orderForm.purchaseOrderId = matchedOrder.id;
-    ElMessage.success(`鎴愬姛閫夋嫨閲囪喘鍗曪細${matchedOrder.orderNo}`);
-    
-    setTimeout(() => {
-        barcodeInputRef.value?.focus();
-    }, 100);
-  }
-};
-
-
-// 鎵弿鏉$爜澶勭悊
+// 鎵弿鏉$爜
 const handleScan = async () => {
-  if (!formRef.value) return;
-  await formRef.value.validateField('barcode');
-
+  if (!isOutboundVerified.value) {
+    ElMessage.warning("璇峰厛楠岃瘉鍑哄簱鍗曟嵁鍙�");
+    playError();
+    nextTick(() => outboundInputRef.value?.focus());
+    return;
+  }
   const barcode = formData.barcode.trim();
-  
-  if (scannedBarcodes.value.some(item => item.barcode === barcode)) {
-    ElMessage.warning(`鏉$爜 ${barcode} 宸叉壂鎻忚繃锛岃鍕块噸澶嶆壂鎻廯);
+  if (!barcode) return;
+  const isDuplicate = scannedBarcodes.value.some(item => item.barcode === barcode);
+  if (isDuplicate) {
+    ElMessage.warning(`鏉$爜銆�${barcode}銆戝凡瀛樺湪`);
+    playError();
     formData.barcode = "";
     nextTick(() => barcodeInputRef.value?.focus());
     return;
   }
-
   try {
     loading.value = true;
-    const res = await http.post("/api/OutboundPicking/BarcodeValidate", {
-      outOder: orderForm.outboundOrderId,
-      inOder: orderForm.purchaseOrderId,
+    let purchaseOrderNo = '';
+    try {
+      purchaseOrderNo = await getPurchaseOrderByBarcode(barcode);
+      if (purchaseOrderNo) orderForm.purchaseOrderNo = purchaseOrderNo;
+    } catch (e) { ElMessage.info("鏈煡璇㈠埌閲囪喘鍗曞彿锛岀户缁獙璇佹潯鐮�"); playError(); }
+    const validateRes = await http.post("/api/OutboundPicking/BarcodeValidate", {
+      outOder: orderForm.outboundOrderNo,
+      inOder: purchaseOrderNo || orderForm.purchaseOrderNo,
       barCode: barcode
     });
-
-    if (res.status === true) {
-      scannedBarcodes.value.push({ barcode });
-      ElMessage.success("鎵弿鎴愬姛");
+    if (validateRes.status === true && Array.isArray(validateRes.data) && validateRes.data.length) {
+      const newItems = validateRes.data.map(item => ({
+        barcode: item.barcode || '',
+        materielCode: item.materielCode || '',
+        materielName: item.materielName || '',
+        batchNo: item.batchNo || '',
+        orderQuantity: item.orderQuantity || item.quantity || 0,
+        supplyCode: item.supplyCode || '',
+        purchaseOrderNo: purchaseOrderNo || ''
+      }));
+      scannedBarcodes.value.push(...newItems);
+      ElMessage.success(`鎵弿鎴愬姛锛屾柊澧� ${newItems.length} 鏉);
+      playSuccess();
       formData.barcode = "";
     } else {
-      ElMessage.error("鎵弿澶辫触锛�" + (res.message || '楠岃瘉澶辫触'));
+      ElMessage.error(validateRes.message || '鏉$爜楠岃瘉澶辫触');
+      playError();
+      formData.barcode = "";
     }
   } catch (error) {
-    ElMessage.error("鎵弿楠岃瘉寮傚父锛�" + error.message);
+    ElMessage.error('鏉$爜楠岃瘉寮傚父');
+    playError();
+    formData.barcode = "";
   } finally {
     loading.value = false;
     nextTick(() => barcodeInputRef.value?.focus());
   }
 };
 
-// 绉婚櫎鍗曟潯鎵弿璁板綍
+const debounce = (fn, delay = 100) => {
+  let timer = null;
+  return (...args) => { if (timer) clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); };
+};
+const debouncedHandleScan = debounce(async (e) => { e.stopPropagation(); e.preventDefault(); await handleScan(); }, 100);
+
+// 鍒犻櫎鏉$爜
 const removeItem = async (index, barcode) => {
   try {
     loading.value = true;
+    const currentItem = scannedBarcodes.value[index];
     const res = await http.post("/api/OutboundPicking/DeleteBarcode", {
-      outOder: orderForm.outboundOrderId,
-      inOder: orderForm.purchaseOrderId,
+      outOder: orderForm.outboundOrderNo,
+      inOder: currentItem?.purchaseOrderNo || orderForm.purchaseOrderNo,
       barCode: barcode
     });
-
     if (res.status === true) {
       scannedBarcodes.value.splice(index, 1);
       ElMessage.success("鍒犻櫎鎴愬姛");
+      if (scannedBarcodes.value.length === 0) orderForm.purchaseOrderNo = "";
     } else {
-      ElMessage.error("鍒犻櫎澶辫触锛�" + (res.message || '鍒犻櫎澶辫触'));
+      ElMessage.error("鍒犻櫎澶辫触锛�" + (res.message || ''));
     }
   } catch (error) {
-    ElMessage.error("鍒犻櫎鏉$爜寮傚父锛�" + error.message);
+    ElMessage.error("鍒犻櫎寮傚父");
   } finally {
     loading.value = false;
+    nextTick(() => barcodeInputRef.value?.focus());
   }
 };
 
 // 鎻愪氦鍑哄簱
 const submit = async () => {
-  if (scannedBarcodes.value.length === 0) {
-    ElMessage.warning("璇峰厛鎵弿鑷冲皯涓�鏉℃潯鐮�");
+  if (!isOutboundVerified.value) {
+    ElMessage.warning("鍑哄簱鍗曟嵁鍙锋湭楠岃瘉");
+    nextTick(() => outboundInputRef.value?.focus());
     return;
   }
-
+  if (scannedBarcodes.value.length === 0) {
+    ElMessage.warning("璇峰厛鎵弿鏉$爜");
+    nextTick(() => barcodeInputRef.value?.focus());
+    return;
+  }
   const barcodes = scannedBarcodes.value.map(item => item.barcode);
-
+  const purchaseOrderNos = [...new Set(scannedBarcodes.value.map(item => item.purchaseOrderNo).filter(Boolean))];
   try {
-    loading.value = true;
+    submitLoading.value = true;
     const res = await http.post("/api/OutboundPicking/NoStockOutSubmit", {
-      OutOderSubmit: orderForm.outboundOrderId,
-      InOderSubmit: orderForm.purchaseOrderId,
+      OutOderSubmit: orderForm.outboundOrderNo,
+      InOderSubmit: purchaseOrderNos.join(',') || '',
       BarCodeSubmit: barcodes
     });
-
     if (res.status === true) {
       ElMessage.success("鍑哄簱鎻愪氦鎴愬姛");
-      showDetailBox.value = false;
+      unlockOutboundOrder();
+      showDetailBox.value = false; // 瑙﹀彂 watch 瑙i攣锛堜絾鎻愪氦鎴愬姛鍚庨�氬父涓嶉渶瑕佽В閿侊紝鍥犱负宸茬粡鍑哄簱浜嗭紝涓嶈繃鍚庣搴旇嚜琛屽鐞嗙姸鎬侊級
+      // 娉ㄦ剰锛氭彁浜ゆ垚鍔熷悗涓嶅簲璇ュ啀璋冪敤瑙i攣鎺ュ彛锛屽洜涓哄崟鎹凡澶勭悊瀹屾瘯銆傝繖閲岄�氳繃鏍囧織閬垮厤锛歴ubmit 鎴愬姛鍚庨噸缃獙璇佺姸鎬�
+      isOutboundVerified.value = false;
+      unlockCalled = true; // 闃叉 watch 鍐嶆璋冪敤瑙i攣
     } else {
-      ElMessage.error("鍑哄簱鎻愪氦澶辫触锛�" + (res.message || '鎻愪氦澶辫触'));
+      unlockOutboundOrder();
+      ElMessage.error("鍑哄簱鎻愪氦澶辫触锛�" + (res.message || ''));
+      nextTick(() => barcodeInputRef.value?.focus());
     }
   } catch (error) {
-    ElMessage.error("鍑哄簱鎻愪氦寮傚父锛�" + error.message);
+    unlockOutboundOrder();
+    ElMessage.error("鎻愪氦寮傚父锛�" + error.message);
+    nextTick(() => barcodeInputRef.value?.focus());
   } finally {
+    submitLoading.value = false;
     loading.value = false;
   }
 };
 
-// 鏆撮湶缁欑埗缁勪欢鐨勬柟娉�
-defineExpose({
-  open
+// 鐢熷懡鍛ㄦ湡锛氭坊鍔犻〉闈㈠叧闂椂鐨勮В閿佺洃鍚�
+onMounted(() => {
+  window.addEventListener('beforeunload', unlockOnPageUnload);
 });
+onBeforeUnmount(() => {
+  window.removeEventListener('beforeunload', unlockOnPageUnload);
+  // 缁勪欢鍗歌浇鏃跺鏋滃脊绐楄繕寮�鐫�锛屼篃灏濊瘯瑙i攣锛堜絾閫氬父椤甸潰鍏抽棴浼氳Е鍙� beforeunload锛�
+  if (showDetailBox.value) {
+    unlockOutboundOrder();
+  }
+});
+
+// 鏆撮湶鏂规硶
+defineExpose({ open });
 </script>
 
 <style scoped>
-/* 鍏抽敭锛氬畾涔夊垪琛ㄩ」鐨勮繃娓″姩鐢� */
+/* 杩囨浮鍔ㄧ敾 */
 .barcode-item-transition-enter-active,
 .barcode-item-transition-leave-active {
   transition: all 0.3s ease;
@@ -454,15 +438,59 @@
   opacity: 0;
   transform: translateX(30px);
 }
-/* 纭繚鍒犻櫎鏃跺叾浠栧厓绱犲钩婊戜笂绉� */
 .barcode-item-transition-move {
   transition: transform 1s ease;
+}
+
+/* 鎻愪氦閬僵灞傛牱寮� */
+.submit-mask {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background-color: rgba(255, 255, 255, 0.85);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  z-index: 100;
+  border-radius: inherit;
+}
+.mask-content {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 12px;
+  color: #409eff;
+  font-size: 15px;
+}
+.loading-icon {
+  font-size: 24px;
+  animation: el-loading-circle 1.5s linear infinite;
+}
+
+/* 楠岃瘉鐘舵�佹爣绛炬牱寮� */
+.verified-tag {
+  color: #67c23a;
+  font-size: 12px;
+  margin-left: 8px;
+  font-weight: 500;
+}
+.loading-tag {
+  color: #409eff;
+  font-size: 12px;
+  margin-left: 8px;
+  font-weight: 500;
+  animation: spin 1s linear infinite;
+}
+@keyframes spin {
+  0% { transform: rotate(0deg); }
+  100% { transform: rotate(360deg); }
 }
 
 .scan-list {
   width: 100%;
 }
-
 .custom-card {
   border-radius: 12px;
   box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08) !important;
@@ -471,7 +499,6 @@
 .custom-card:hover {
   box-shadow: 0 10px 30px rgba(0, 0, 0, 0.12) !important;
 }
-
 .card-header {
   display: flex;
   justify-content: space-between;
@@ -485,12 +512,9 @@
   font-size: 15px;
   color: #333;
 }
-
 .card-body {
   padding: 0;
 }
-
-/* 鑷畾涔夋粴鍔ㄦ潯 */
 .custom-scrollbar :deep(.el-scrollbar__thumb) {
   background: rgba(0, 0, 0, 0.2);
   border-radius: 4px;
@@ -503,37 +527,58 @@
 .custom-scrollbar :deep(.el-scrollbar__wrap) {
   overflow-x: hidden;
 }
-
 .barcode-item {
   display: flex;
   justify-content: space-between;
-  align-items: center;
-  padding: 10px 15px;
+  align-items: flex-start;
+  padding: 15px;
   border-bottom: 1px solid #f7f7f7;
   transition: background-color 0.2s ease;
 }
 .barcode-item:hover {
   background-color: #fafafa;
 }
-/* 涓哄鏁拌娣诲姞杞诲井鐨勮儗鏅壊锛屽寮哄彲璇绘�� */
 .barcode-item:nth-child(odd) {
   background-color: #f9f9f9;
 }
-.barcode-text {
+.barcode-detail {
   flex: 1;
+  display: grid;
+  grid-template-columns: repeat(4, 1fr);
+  gap: 8px 15px;
   font-size: 14px;
+}
+@media (max-width: 1200px) {
+  .barcode-detail {
+    grid-template-columns: repeat(3, 1fr);
+  }
+}
+@media (max-width: 992px) {
+  .barcode-detail {
+    grid-template-columns: repeat(2, 1fr);
+  }
+}
+.detail-row {
+  display: flex;
+  align-items: center;
+}
+.label {
+  color: #999;
+  margin-right: 5px;
+  white-space: nowrap;
+}
+.value {
   color: #666;
-  transition: color 0.2s;
+  flex: 1;
+  word-break: break-all;
 }
-.barcode-item:hover .barcode-text {
-  color: #409eff;
-}
-
 .delete-btn {
   color: #ea1919;
   font-size: 16px;
   transition: all 0.2s;
   opacity: 0.7;
+  margin-left: 10px;
+  flex-shrink: 0;
 }
 .barcode-item:hover .delete-btn {
   opacity: 1;
@@ -542,7 +587,6 @@
   color: #f56c6c !important;
   background: rgba(245, 108, 108, 0.1);
 }
-
 .empty-tip {
   text-align: center;
   padding: 80px 0;
@@ -558,8 +602,6 @@
   font-size: 40px;
   color: #dcdfe6;
 }
-
-/* 鑷畾涔夎緭鍏ユ */
 .custom-input :deep(.el-input__inner) {
   border-radius: 6px;
   border-color: #e4e7ed;
@@ -571,8 +613,6 @@
   border-color: #409eff;
   box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.1);
 }
-
-/* 鑷畾涔夋寜閽� */
 .custom-button {
   border-radius: 6px;
   height: 36px;
@@ -585,7 +625,6 @@
   transform: translateY(-1px);
   box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
 }
-
 .footer-actions {
   text-align: right;
 }
@@ -611,7 +650,6 @@
 </style>
 
 <style>
-/* 鍏ㄥ眬鏍峰紡閮ㄥ垎淇濇寔涓嶅彉 */
 .text-button:hover {
   background-color: #f0f9eb !important;
 }

--
Gitblit v1.9.3