From a6baad20258a61d9ce9a786029ca4cb63a7c992e Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期二, 21 四月 2026 17:31:33 +0800
Subject: [PATCH] 优化出入库功能,新增对未完全入库和出库商品的检查,更新相关API接口以支持审批人字段的统一命名

---
 src/pages/inventoryManagement/scanIn/index.vue |  323 +++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 files changed, 309 insertions(+), 14 deletions(-)

diff --git a/src/pages/inventoryManagement/scanIn/index.vue b/src/pages/inventoryManagement/scanIn/index.vue
index 8479acd..f68ebd5 100644
--- a/src/pages/inventoryManagement/scanIn/index.vue
+++ b/src/pages/inventoryManagement/scanIn/index.vue
@@ -99,7 +99,7 @@
             <text class="kv-label">鍏ュ簱鏁伴噺</text>
             <view class="kv-value stocked-qty-input-wrap">
               <up-input :key="'stocked-' + idx"
-                        v-model="item.stockedQuantity"
+                        v-model="item.operateQuantity"
                         type="number"
                         placeholder="璇疯緭鍏ュ叆搴撴暟閲�"
                         clearable
@@ -109,6 +109,51 @@
           </view>
         </view>
       </view>
+      <view class="approval-process">
+        <view class="approval-header">
+          <text class="approval-title">瀹℃牳娴佺▼</text>
+          <text class="approval-desc">姣忎釜姝ラ鍙兘閫夋嫨涓�涓鎵逛汉</text>
+        </view>
+        <view class="approval-steps">
+          <view v-for="(step, stepIndex) in stockApproverNodes"
+                :key="step.id"
+                class="approval-step">
+            <view class="step-title">
+              <text>瀹℃壒浜�</text>
+            </view>
+            <view class="approver-container">
+              <view v-if="step.userName"
+                    class="approver-item">
+                <view class="approver-avatar">
+                  <text class="avatar-text">{{ step.userName.charAt(0) }}</text>
+                </view>
+                <view class="approver-info">
+                  <text class="approver-name">{{ step.userName }}</text>
+                </view>
+                <view class="delete-approver-btn"
+                      @click="removeApprover(stepIndex)">脳</view>
+              </view>
+              <view v-else
+                    class="add-approver-btn"
+                    @click="openApproverPicker(stepIndex)">
+                <view class="add-circle">+</view>
+                <text class="add-label">閫夋嫨瀹℃壒浜�</text>
+              </view>
+            </view>
+            <view class="delete-step-btn"
+                  v-if="stockApproverNodes.length > 1"
+                  @click="removeStockApproverNode(stepIndex)">鍒犻櫎鑺傜偣</view>
+          </view>
+        </view>
+        <view class="add-step-btn">
+          <u-button icon="plus"
+                    plain
+                    type="primary"
+                    style="width: 100%"
+                    @click="addStockApproverNode">鏂板鑺傜偣</u-button>
+        </view>
+      </view>
+
       <view class="footer-btns">
         <u-button class="footer-cancel-btn"
                   @click="cancelForm">杩斿洖</u-button>
@@ -117,11 +162,12 @@
                   @click="confirmInbound">纭鍏ュ簱</u-button>
       </view>
     </scroll-view>
+
   </view>
 </template>
 
 <script setup>
-  import { ref, computed } from "vue";
+  import { ref, computed, onMounted, onUnmounted } from "vue";
   import PageHeader from "@/components/PageHeader.vue";
   import { productList as salesProductList } from "@/api/salesManagement/salesLedger";
   import modal from "@/plugins/modal";
@@ -146,6 +192,8 @@
   const contractKind = ref(CONTRACT_KIND.sales);
   const scanLedgerId = ref(null);
   const submitLoading = ref(false);
+  const stockApproverNodes = ref([{ id: 1, userId: null, userName: "" }]);
+  let nextApproverNodeId = 2;
   const submitConfigByScene = createSubmitConfig(scanLedgerId);
 
   const cardTitleMain = computed(() => {
@@ -166,7 +214,22 @@
     };
   };
 
-  const { detailFieldRows, summaryFieldRows } = useScanOutFieldRows(contractKind);
+  const { detailFieldRows: rawDetailFieldRows, summaryFieldRows: rawSummaryFieldRows } = useScanOutFieldRows(
+    contractKind,
+    "inbound"
+  );
+  const shouldShowInboundQuantityField = key => {
+    if (type.value === QUALITY_TYPE.qualified) return key !== "unqualifiedStockedQuantity";
+    if (type.value === QUALITY_TYPE.unqualified)
+      return key !== "stockedQuantity" && key !== "remainingQuantity";
+    return true;
+  };
+  const detailFieldRows = computed(() =>
+    rawDetailFieldRows.value.filter(row => shouldShowInboundQuantityField(row.key))
+  );
+  const summaryFieldRows = computed(() =>
+    rawSummaryFieldRows.value.filter(row => shouldShowInboundQuantityField(row.key))
+  );
 
   const emptyDash = v => {
     if (v === null || v === undefined || v === "") return "-";
@@ -211,25 +274,39 @@
     return emptyDash(v);
   };
 
+  const shouldValidateStockStatus = computed(() => {
+    return (
+      contractKind.value === CONTRACT_KIND.sales &&
+      type.value === QUALITY_TYPE.qualified
+    );
+  });
+
   const isFullyStocked = item => {
+    if (!shouldValidateStockStatus.value) return false;
     const s = item?.productStockStatus;
     return s == 2 || s === "2";
   };
 
   const onStockedQtyBlur = item => {
     if (isFullyStocked(item)) return;
-    const raw = item.stockedQuantity;
+    const raw = item.operateQuantity;
     if (raw === null || raw === undefined || String(raw).trim() === "") {
-      item.stockedQuantity = "0";
+      item.operateQuantity = "0";
       return;
     }
     const n = Number(String(raw).trim());
     if (Number.isNaN(n)) {
-      item.stockedQuantity = defaultStockedQuantityFromRow(item);
+      item.operateQuantity =
+        type.value === QUALITY_TYPE.unqualified ? "0" : defaultStockedQuantityFromRow(item, "inbound");
       return;
     }
-    item.stockedQuantity = String(Math.max(0, n));
+    item.operateQuantity = String(Math.max(0, n));
   };
+
+  const hasEditableInboundItems = computed(() => {
+    if (!recordList.value?.length) return false;
+    return recordList.value.some(item => !isFullyStocked(item));
+  });
 
   const formatCell = (item, row, idx) => {
     if (row.key === "index") {
@@ -242,11 +319,35 @@
       return formatProductStockStatus(item.productStockStatus);
     if (row.key === "heavyBox") return formatHeavyBox(item.heavyBox);
     if (row.key === "remainingQuantity") {
+      const v = item.remainingQuantity;
+      return emptyDash(v);
+    }
+    if (row.key === "remainingShippedQuantity") {
+      const v = item.remainingShippedQuantity;
+      return emptyDash(v);
+    }
+    if (row.key === "shippedQuantity") {
+      const v = item.shippedQuantity;
+      return emptyDash(v);
+    }
+    if (row.key === "unqualifiedShippedQuantity") {
       const v =
-        item.remainingQuantity ??
-        item.remaining_quantity ??
-        item.remainQuantity ??
-        item.remain_quantity;
+        item.unqualifiedShippedQuantity ??
+        item.unQualifiedShippedQuantity ??
+        item.unqualifiedShippedQty ??
+        item.unqualifiedOutboundQuantity;
+      return emptyDash(v);
+    }
+    if (row.key === "stockedQuantity") {
+      const v = item.stockedQuantity;
+      return emptyDash(v);
+    }
+    if (row.key === "unqualifiedStockedQuantity") {
+      const v =
+        item.unqualifiedStockedQuantity ??
+        item.unQualifiedStockedQuantity ??
+        item.unqualifiedStockedQty ??
+        item.unqualifiedInboundQuantity;
       return emptyDash(v);
     }
     if (row.key === "availableQuality") {
@@ -267,11 +368,71 @@
     scanLedgerId.value = null;
     expandedByIndex.value = {};
     recordList.value = [];
+    stockApproverNodes.value = [{ id: 1, userId: null, userName: "" }];
   };
 
-  const confirmInbound = async () => {
+  onMounted(() => {
+    uni.$on("selectContact", handleSelectContact);
+  });
+
+  onUnmounted(() => {
+    uni.$off("selectContact", handleSelectContact);
+  });
+
+  const addStockApproverNode = () => {
+    stockApproverNodes.value.push({
+      id: nextApproverNodeId++,
+      userId: null,
+      userName: "",
+    });
+  };
+
+  const removeStockApproverNode = index => {
+    if (stockApproverNodes.value.length <= 1) {
+      modal.msgError("鑷冲皯淇濈暀涓�涓鎵硅妭鐐�");
+      return;
+    }
+    stockApproverNodes.value.splice(index, 1);
+  };
+
+  const removeApprover = stepIndex => {
+    if (!stockApproverNodes.value[stepIndex]) return;
+    stockApproverNodes.value[stepIndex].userId = null;
+    stockApproverNodes.value[stepIndex].userName = "";
+  };
+
+  const openApproverPicker = index => {
+    uni.setStorageSync("stepIndex", index);
+    uni.navigateTo({
+      url: "/pages/cooperativeOffice/collaborativeApproval/contactSelect?approveType=9",
+    });
+  };
+
+  const handleSelectContact = data => {
+    const { stepIndex, contact } = data || {};
+    if (stepIndex === null || stepIndex === undefined) return;
+    const idx = Number(stepIndex);
+    if (Number.isNaN(idx) || !stockApproverNodes.value[idx]) return;
+    stockApproverNodes.value[idx].userId = contact?.userId ?? null;
+    stockApproverNodes.value[idx].userName = contact?.nickName || contact?.userName || "";
+  };
+
+  const validateApproverNodes = () => {
+    const hasEmptyNode = stockApproverNodes.value.some(node => !node.userId);
+    if (hasEmptyNode) {
+      modal.msgError("璇蜂负姣忎釜瀹℃壒鑺傜偣閫夋嫨瀹℃壒浜�");
+      return false;
+    }
+    return true;
+  };
+
+  const submitInbound = async () => {
     if (scanLedgerId.value == null || scanLedgerId.value === "") {
       modal.msgError("缂哄皯璁㈠崟淇℃伅锛岃閲嶆柊鎵爜");
+      return;
+    }
+    if (!hasEditableInboundItems.value) {
+      modal.msgError("璇ヤ骇鍝佸凡缁忓叏閮ㄥ叆搴�");
       return;
     }
     const salesLedgerProductList = buildSalesLedgerProductList(recordList.value);
@@ -286,7 +447,11 @@
       return;
     }
     const runApi = currentSubmitConfig.runApi;
-    const payload = currentSubmitConfig.payloadBuilder(salesLedgerProductList);
+    const approveUserIds = stockApproverNodes.value.map(node => node.userId).join(",");
+    const payload = currentSubmitConfig.payloadBuilder(
+      salesLedgerProductList,
+      approveUserIds
+    );
     try {
       submitLoading.value = true;
       modal.loading("鎻愪氦涓�...");
@@ -304,6 +469,11 @@
     } finally {
       submitLoading.value = false;
     }
+  };
+
+  const confirmInbound = () => {
+    if (!validateApproverNodes()) return;
+    submitInbound();
   };
 
   const goBack = () => {
@@ -351,7 +521,18 @@
       if (res.code === 200 && res.data && res.data.length > 0) {
         recordList.value = res.data.map(row => ({
           ...row,
-          stockedQuantity: defaultStockedQuantityFromRow(row),
+          unqualifiedShippedQuantity:
+            row.unqualifiedShippedQuantity ??
+            row.unQualifiedShippedQuantity ??
+            row.unqualifiedShippedQty ??
+            row.unqualifiedOutboundQuantity,
+          unqualifiedStockedQuantity:
+            row.unqualifiedStockedQuantity ??
+            row.unQualifiedStockedQuantity ??
+            row.unqualifiedStockedQty ??
+            row.unqualifiedInboundQuantity,
+          operateQuantity:
+            type.value === QUALITY_TYPE.unqualified ? "0" : defaultStockedQuantityFromRow(row, "inbound"),
         }));
         expandedByIndex.value = {};
         showForm.value = true;
@@ -579,4 +760,118 @@
     color: #fff;
     border: none;
   }
+
+  .approval-process {
+    background: #fff;
+    margin: 20rpx;
+    border-radius: 16rpx;
+    padding: 24rpx;
+  }
+
+  .approval-header {
+    margin-bottom: 16rpx;
+  }
+
+  .approval-title {
+    font-size: 30rpx;
+    font-weight: 600;
+    color: #333;
+    display: block;
+  }
+
+  .approval-desc {
+    font-size: 24rpx;
+    color: #999;
+    margin-top: 6rpx;
+  }
+
+  .approval-step {
+    margin-bottom: 18rpx;
+  }
+
+  .step-title text {
+    font-size: 24rpx;
+    color: #666;
+  }
+
+  .approver-container {
+    display: flex;
+    align-items: center;
+    margin-top: 10rpx;
+  }
+
+  .approver-item {
+    width: 100%;
+    display: flex;
+    align-items: center;
+    gap: 12rpx;
+    padding: 12rpx 0;
+  }
+
+  .approver-avatar {
+    width: 64rpx;
+    height: 64rpx;
+    border-radius: 50%;
+    background: #f3f4f6;
+    border: 2rpx solid #e5e7eb;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+
+  .avatar-text {
+    font-size: 24rpx;
+    color: #374151;
+    font-weight: 600;
+  }
+
+  .approver-info {
+    flex: 1;
+  }
+
+  .approver-name {
+    font-size: 28rpx;
+    color: #333;
+  }
+
+  .delete-approver-btn {
+    font-size: 32rpx;
+    color: #ff4d4f;
+    padding: 0 8rpx;
+  }
+
+  .add-approver-btn {
+    display: flex;
+    align-items: center;
+    gap: 10rpx;
+    color: #3b82f6;
+    padding: 10rpx 0;
+  }
+
+  .add-circle {
+    width: 52rpx;
+    height: 52rpx;
+    border: 2rpx dashed #a0aec0;
+    border-radius: 50%;
+    color: #6b7280;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 34rpx;
+    line-height: 1;
+  }
+
+  .add-label {
+    font-size: 26rpx;
+  }
+
+  .delete-step-btn {
+    color: #ff4d4f;
+    font-size: 24rpx;
+    margin-top: 8rpx;
+  }
+
+  .add-step-btn {
+    margin-top: 8rpx;
+  }
 </style>

--
Gitblit v1.9.3