From 8aae660d1dd2455d300d7738509f12b33d3865e0 Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期六, 18 四月 2026 15:24:48 +0800
Subject: [PATCH] 新增销售和采购订单扫码入库功能的前端页面支持,优化API接口以处理合格和不合格入库情况

---
 src/pages/inventoryManagement/scanIn/index.vue |  559 +++++++++++++++++++++++++++++++++++++++++--------------
 1 files changed, 417 insertions(+), 142 deletions(-)

diff --git a/src/pages/inventoryManagement/scanIn/index.vue b/src/pages/inventoryManagement/scanIn/index.vue
index 3e17f31..8479acd 100644
--- a/src/pages/inventoryManagement/scanIn/index.vue
+++ b/src/pages/inventoryManagement/scanIn/index.vue
@@ -13,7 +13,7 @@
         </view>
         <view class="module-info">
           <text class="module-label">鍚堟牸鍏ュ簱</text>
-          <text class="module-desc">鎵弿鍚堟牸浜у搧淇℃伅</text>
+          <text class="module-desc">鎵弿鍚堟牸浜у搧杩涜鍏ュ簱</text>
         </view>
       </view>
       <view class="module-card"
@@ -25,124 +25,297 @@
         </view>
         <view class="module-info">
           <text class="module-label">涓嶅悎鏍煎叆搴�</text>
-          <text class="module-desc">褰曞叆涓嶅悎鏍煎搧璁板綍</text>
+          <text class="module-desc">璁板綍涓嶅悎鏍煎搧鐨勫叆搴�</text>
         </view>
       </view>
     </view>
-    <view class="form-content"
-          v-if="showForm">
-      <u-form ref="formRef"
-              :model="form"
-              :rules="formRules"
-              label-width="100px">
-        <u-form-item label="鍏ュ簱绫诲瀷"
-                     border-bottom>
-          <u-tag :text="type === 'qualified' ? '鍚堟牸鍏ュ簱' : '涓嶅悎鏍煎叆搴�'"
-                 :type="type === 'qualified' ? 'success' : 'error'"></u-tag>
-        </u-form-item>
-        <u-form-item label="浜у搧鍚嶇О"
-                     border-bottom>
-          <u-input v-model="form.productName"
-                   readonly
-                   border="none"></u-input>
-        </u-form-item>
-        <u-form-item label="瑙勬牸鍨嬪彿"
-                     border-bottom>
-          <u-input v-model="form.productModelName"
-                   readonly
-                   border="none"></u-input>
-        </u-form-item>
-        <u-form-item label="鍗曚綅"
-                     border-bottom>
-          <u-input v-model="form.unit"
-                   readonly
-                   border="none"></u-input>
-        </u-form-item>
-        <u-form-item label="鍏ュ簱鏁伴噺"
-                     prop="qualitity"
-                     required
-                     border-bottom>
-          <u-number-box v-model="form.qualitity"
-                        :min="1"
-                        :step="1"></u-number-box>
-        </u-form-item>
-        <u-form-item label="棰勮鏁伴噺"
-                     prop="warnNum"
-                     v-if="type === 'qualified'"
-                     border-bottom>
-          <u-number-box v-model="form.warnNum"
-                        :min="0"
-                        :step="1"></u-number-box>
-        </u-form-item>
-        <u-form-item label="澶囨敞"
-                     prop="remark"
-                     border-bottom>
-          <u-textarea v-model="form.remark"
-                      placeholder="璇疯緭鍏ュ娉�"
-                      count></u-textarea>
-        </u-form-item>
-      </u-form>
-      <view class="footer-btns">
-        <u-button class="cancel-btn"
-                  @click="cancelForm">鍙栨秷</u-button>
-        <u-button class="save-btn"
-                  @click="handleSubmit"
-                  :loading="loading">纭鍏ュ簱</u-button>
+    <scroll-view v-if="showForm"
+                 scroll-y
+                 class="detail-scroll">
+      <view class="detail-card"
+            v-for="(item, idx) in recordList"
+            :key="idx">
+        <view class="detail-card-title"
+              :class="{
+                'detail-card-title--collapsible': recordList.length > 1,
+                'is-collapsed': recordList.length > 1 && !isCardExpanded(idx),
+              }"
+              @click="recordList.length > 1 && toggleCardDetail(idx)">
+          <view class="detail-card-title-text">
+            <text class="detail-card-title-no">{{ cardTitleMain }}</text>
+            <text v-if="recordList.length > 1"
+                  class="detail-card-title-seq">锛坽{ idx + 1 }}/{{ recordList.length }}锛�</text>
+          </view>
+          <u-icon v-if="recordList.length > 1"
+                  :name="isCardExpanded(idx) ? 'arrow-up' : 'arrow-down'"
+                  color="#999"
+                  size="18"></u-icon>
+        </view>
+        <view v-show="recordList.length > 1 && !isCardExpanded(idx)"
+              class="detail-card-summary"
+              @click="toggleCardDetail(idx)">
+          <view class="kv-row kv-row--summary"
+                v-for="row in summaryFieldRows"
+                :key="'sum-' + row.key">
+            <text class="kv-label">{{ row.label }}</text>
+            <view class="kv-value kv-value--tag"
+                  v-if="row.key === 'approveStatus'">
+              <u-tag :type="approveStatusTagType(item)"
+                     size="small">{{ formatApproveStatus(item) }}</u-tag>
+            </view>
+            <view class="kv-value kv-value--tag"
+                  v-else-if="row.key === 'productStockStatus'">
+              <u-tag :type="productStockStatusTagType(item.productStockStatus)"
+                     size="small">{{ formatProductStockStatus(item.productStockStatus) }}</u-tag>
+            </view>
+            <text class="kv-value"
+                  v-else>{{ formatCell(item, row, idx) }}</text>
+          </view>
+          <text class="summary-tip">鐐瑰嚮鏌ョ湅鍏ㄩ儴</text>
+        </view>
+        <view v-show="isCardExpanded(idx)"
+              class="detail-card-body">
+          <view class="kv-row"
+                v-for="row in detailFieldRows"
+                :key="row.key">
+            <text class="kv-label">{{ row.label }}</text>
+            <view class="kv-value kv-value--tag"
+                  v-if="row.key === 'approveStatus'">
+              <u-tag :type="approveStatusTagType(item)"
+                     size="small">{{ formatApproveStatus(item) }}</u-tag>
+            </view>
+            <view class="kv-value kv-value--tag"
+                  v-else-if="row.key === 'productStockStatus'">
+              <u-tag :type="productStockStatusTagType(item.productStockStatus)"
+                     size="small">{{ formatProductStockStatus(item.productStockStatus) }}</u-tag>
+            </view>
+            <text class="kv-value"
+                  v-else>{{ formatCell(item, row, idx) }}</text>
+          </view>
+        </view>
+        <view v-if="!isFullyStocked(item)"
+              class="stocked-qty-block">
+          <view class="kv-row stocked-qty-row">
+            <text class="kv-label">鍏ュ簱鏁伴噺</text>
+            <view class="kv-value stocked-qty-input-wrap">
+              <up-input :key="'stocked-' + idx"
+                        v-model="item.stockedQuantity"
+                        type="number"
+                        placeholder="璇疯緭鍏ュ叆搴撴暟閲�"
+                        clearable
+                        border="surround"
+                        @blur="onStockedQtyBlur(item)" />
+            </view>
+          </view>
+        </view>
       </view>
-    </view>
+      <view class="footer-btns">
+        <u-button class="footer-cancel-btn"
+                  @click="cancelForm">杩斿洖</u-button>
+        <u-button class="footer-confirm-btn"
+                  :loading="submitLoading"
+                  @click="confirmInbound">纭鍏ュ簱</u-button>
+      </view>
+    </scroll-view>
   </view>
 </template>
 
 <script setup>
-  import { ref, reactive } from "vue";
+  import { ref, computed } from "vue";
   import PageHeader from "@/components/PageHeader.vue";
-  import {
-    createStockInventory,
-    getStockInventoryListPage,
-  } from "@/api/inventoryManagement/stockInventory.js";
-  import {
-    createStockUnInventory,
-    getStockUninventoryListPage,
-  } from "@/api/inventoryManagement/stockUninventory.js";
+  import { productList as salesProductList } from "@/api/salesManagement/salesLedger";
   import modal from "@/plugins/modal";
+  import { QUALITY_TYPE, CONTRACT_KIND } from "../scanOut/scanOut.constants";
+  import { useScanOutFieldRows } from "../scanOut/scanOut.fields";
+  import {
+    defaultStockedQuantityFromRow,
+    resolveQrContractKind,
+    resolveListTypeForDetail,
+    resolveContractNo,
+    buildSalesLedgerProductList,
+    hasAnyPositiveStockedQty,
+    resolveSubmitSceneKey,
+  } from "../scanOut/scanOut.logic";
+  import { createSubmitConfig } from "./scanIn.submit";
 
   const showForm = ref(false);
-  const type = ref("qualified"); // qualified | unqualified
-  const loading = ref(false);
-  const formRef = ref(null);
+  const type = ref(QUALITY_TYPE.qualified);
+  const recordList = ref([]);
+  const expandedByIndex = ref({});
+  const scanContractNo = ref("");
+  const contractKind = ref(CONTRACT_KIND.sales);
+  const scanLedgerId = ref(null);
+  const submitLoading = ref(false);
+  const submitConfigByScene = createSubmitConfig(scanLedgerId);
 
-  const form = ref({
-    productId: undefined,
-    productModelId: undefined,
-    productName: "",
-    productModelName: "",
-    unit: "",
-    qualitity: 1,
-    warnNum: 0,
-    remark: "",
+  const cardTitleMain = computed(() => {
+    const no = scanContractNo.value?.trim();
+    return no || "鈥�";
   });
 
-  const formRules = {
-    qualitity: [
-      {
-        required: true,
-        type: "number",
-        message: "璇疯緭鍏ュ叆搴撴暟閲�",
-        trigger: ["blur", "change"],
-      },
-    ],
+  const isCardExpanded = idx => {
+    if (recordList.value.length <= 1) return true;
+    return !!expandedByIndex.value[idx];
+  };
+
+  const toggleCardDetail = idx => {
+    if (recordList.value.length <= 1) return;
+    expandedByIndex.value = {
+      ...expandedByIndex.value,
+      [idx]: !expandedByIndex.value[idx],
+    };
+  };
+
+  const { detailFieldRows, summaryFieldRows } = useScanOutFieldRows(contractKind);
+
+  const emptyDash = v => {
+    if (v === null || v === undefined || v === "") return "-";
+    return v;
+  };
+
+  const formatApproveStatus = row => {
+    const a = row.approveStatus;
+    const noShipInfo = !row.shippingDate || !row.shippingCarNumber;
+    const hasShipInfo = !!(row.shippingDate || row.shippingCarNumber);
+    if ((a === 1 || a === "1") && noShipInfo) return "鍏呰冻";
+    if ((a === 0 || a === "0") && hasShipInfo) return "宸插嚭搴�";
+    return "涓嶈冻";
+  };
+
+  const formatProductStockStatus = v => {
+    if (v == 1) return "閮ㄥ垎鍏ュ簱";
+    if (v == 2) return "宸插叆搴�";
+    if (v == 0) return "鏈嚭搴�";
+    return "涓嶈冻";
+  };
+
+  const approveStatusTagType = row => {
+    const a = row.approveStatus;
+    const noShipInfo = !row.shippingDate || !row.shippingCarNumber;
+    const hasShipInfo = !!(row.shippingDate || row.shippingCarNumber);
+    if ((a === 1 || a === "1") && noShipInfo) return "success";
+    if ((a === 0 || a === "0") && hasShipInfo) return "success";
+    return "error";
+  };
+
+  const productStockStatusTagType = v => {
+    if (v == 1) return "warning";
+    if (v == 2) return "success";
+    if (v == 0) return "info";
+    return "error";
+  };
+
+  const formatHeavyBox = v => {
+    if (v === 1 || v === true || v === "1") return "鏄�";
+    if (v === 0 || v === false || v === "0") return "鍚�";
+    return emptyDash(v);
+  };
+
+  const isFullyStocked = item => {
+    const s = item?.productStockStatus;
+    return s == 2 || s === "2";
+  };
+
+  const onStockedQtyBlur = item => {
+    if (isFullyStocked(item)) return;
+    const raw = item.stockedQuantity;
+    if (raw === null || raw === undefined || String(raw).trim() === "") {
+      item.stockedQuantity = "0";
+      return;
+    }
+    const n = Number(String(raw).trim());
+    if (Number.isNaN(n)) {
+      item.stockedQuantity = defaultStockedQuantityFromRow(item);
+      return;
+    }
+    item.stockedQuantity = String(Math.max(0, n));
+  };
+
+  const formatCell = (item, row, idx) => {
+    if (row.key === "index") {
+      const v = item.index;
+      if (v !== null && v !== undefined && v !== "") return String(v);
+      return String(idx + 1);
+    }
+    if (row.key === "approveStatus") return formatApproveStatus(item);
+    if (row.key === "productStockStatus")
+      return formatProductStockStatus(item.productStockStatus);
+    if (row.key === "heavyBox") return formatHeavyBox(item.heavyBox);
+    if (row.key === "remainingQuantity") {
+      const v =
+        item.remainingQuantity ??
+        item.remaining_quantity ??
+        item.remainQuantity ??
+        item.remain_quantity;
+      return emptyDash(v);
+    }
+    if (row.key === "availableQuality") {
+      const v = item.availableQuality ?? item.availableQuantity;
+      return emptyDash(v);
+    }
+    if (row.key === "returnQuality") {
+      const v = item.returnQuality ?? item.returnQuantity;
+      return emptyDash(v);
+    }
+    return emptyDash(item[row.key]);
+  };
+
+  const resetDetailView = () => {
+    showForm.value = false;
+    scanContractNo.value = "";
+    contractKind.value = CONTRACT_KIND.sales;
+    scanLedgerId.value = null;
+    expandedByIndex.value = {};
+    recordList.value = [];
+  };
+
+  const confirmInbound = async () => {
+    if (scanLedgerId.value == null || scanLedgerId.value === "") {
+      modal.msgError("缂哄皯璁㈠崟淇℃伅锛岃閲嶆柊鎵爜");
+      return;
+    }
+    const salesLedgerProductList = buildSalesLedgerProductList(recordList.value);
+    if (!hasAnyPositiveStockedQty(salesLedgerProductList)) {
+      modal.msgError("璇疯嚦灏戝~鍐欎竴琛屽ぇ浜� 0 鐨勫叆搴撴暟閲�");
+      return;
+    }
+    const sceneKey = resolveSubmitSceneKey(contractKind.value, type.value);
+    const currentSubmitConfig = submitConfigByScene[sceneKey];
+    if (!currentSubmitConfig) {
+      modal.msgError("鏆備笉鏀寔褰撳墠鍏ュ簱鍦烘櫙");
+      return;
+    }
+    const runApi = currentSubmitConfig.runApi;
+    const payload = currentSubmitConfig.payloadBuilder(salesLedgerProductList);
+    try {
+      submitLoading.value = true;
+      modal.loading("鎻愪氦涓�...");
+      const res = await runApi(payload);
+      modal.closeLoading();
+      if (res.code === 200) {
+        modal.msgSuccess("鍏ュ簱鎴愬姛");
+        resetDetailView();
+      } else {
+        modal.msgError(res.msg || "鎻愪氦澶辫触");
+      }
+    } catch (e) {
+      modal.closeLoading();
+      console.error("鎵爜鍏ュ簱鎻愪氦澶辫触", e);
+    } finally {
+      submitLoading.value = false;
+    }
   };
 
   const goBack = () => {
     if (showForm.value) {
-      showForm.value = false;
+      resetDetailView();
     } else {
       uni.navigateBack();
     }
   };
 
   const cancelForm = () => {
-    showForm.value = false;
+    resetDetailView();
   };
 
   const startScan = scanType => {
@@ -151,7 +324,7 @@
       success: res => {
         handleScanResult(res.result);
       },
-      fail: err => {
+      fail: () => {
         modal.msgError("鎵爜澶辫触");
       },
     });
@@ -159,52 +332,38 @@
 
   const handleScanResult = async result => {
     try {
-      // 瑙f瀽浜岀淮鐮佹暟鎹�
       const scanData = JSON.parse(result);
       if (!scanData.id) {
         modal.msgError("鏃犳晥鐨勪簩缁寸爜鏁版嵁");
         return;
       }
-
-      // 鐩存帴浠庝簩缁寸爜淇℃伅涓幏鍙栦骇鍝佽鎯�
-      form.value.productId = scanData.productId; // 濡傛灉浜岀淮鐮佷腑鏈� productId
-      form.value.productName = scanData.productName;
-      form.value.productModelId = scanData.id; // 浜岀淮鐮佷腑鐨� id 鏄骇鍝佸瀷鍙� ID
-      form.value.productModelName = scanData.model;
-      form.value.unit = scanData.unit;
-      form.value.qualitity = 1;
-      form.value.warnNum = 0;
-      form.value.remark = "";
-
-      showForm.value = true;
-    } catch (error) {
-      console.error("瑙f瀽浜岀淮鐮佸け璐�", error);
-      modal.msgError("瑙f瀽浜岀淮鐮佸け璐ワ紝璇风‘淇濇壂鐮佸唴瀹规纭�");
-    }
-  };
-
-  const handleSubmit = async () => {
-    try {
-      const valid = await formRef.value.validate();
-      if (!valid) return;
-
-      loading.value = true;
-      const apiCall =
-        type.value === "qualified"
-          ? createStockInventory
-          : createStockUnInventory;
-
-      const res = await apiCall(form.value);
-      if (res.code === 200) {
-        modal.msgSuccess("鍏ュ簱鎴愬姛");
-        setTimeout(() => {
-          showForm.value = false;
-        }, 1500);
+      const kind = resolveQrContractKind(scanData);
+      contractKind.value = kind;
+      scanLedgerId.value = scanData.id;
+      scanContractNo.value = resolveContractNo(scanData, kind);
+      const listType = resolveListTypeForDetail(kind);
+      modal.loading("鑾峰彇浜у搧搴撳瓨璇︽儏...");
+      const res = await salesProductList({
+        salesLedgerId: scanData.id,
+        type: listType,
+      });
+      modal.closeLoading();
+      if (res.code === 200 && res.data && res.data.length > 0) {
+        recordList.value = res.data.map(row => ({
+          ...row,
+          stockedQuantity: defaultStockedQuantityFromRow(row),
+        }));
+        expandedByIndex.value = {};
+        showForm.value = true;
+      } else {
+        scanLedgerId.value = null;
+        modal.msgError("鏈煡璇㈠埌鏄庣粏鏁版嵁");
       }
     } catch (error) {
-      console.error("鎻愪氦澶辫触", error);
-    } finally {
-      loading.value = false;
+      modal.closeLoading();
+      scanLedgerId.value = null;
+      console.error("澶勭悊鎵爜缁撴灉澶辫触", error);
+      modal.msgError("鎵爜澶勭悊澶辫触锛岃閲嶈瘯");
     }
   };
 </script>
@@ -277,29 +436,145 @@
     color: #999;
   }
 
-  .form-content {
+  .detail-scroll {
+    max-height: calc(100vh - 120rpx);
+    box-sizing: border-box;
+  }
+
+  .detail-card {
     background-color: #fff;
     margin: 20rpx;
-    padding: 30rpx;
+    padding: 28rpx;
     border-radius: 16rpx;
+    box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
+  }
+
+  .detail-card-title {
+    font-size: 30rpx;
+    font-weight: 600;
+    color: #333;
+    margin-bottom: 20rpx;
+    padding-bottom: 16rpx;
+    border-bottom: 1rpx solid #eee;
+  }
+
+  .detail-card-title--collapsible {
+    display: flex;
+    flex-direction: row;
+    justify-content: space-between;
+    align-items: center;
+  }
+
+  .detail-card-title--collapsible.is-collapsed {
+    margin-bottom: 0;
+    padding-bottom: 0;
+    border-bottom: none;
+  }
+
+  .detail-card-title-text {
+    flex: 1;
+    min-width: 0;
+    margin-right: 16rpx;
+    display: flex;
+    flex-wrap: wrap;
+    align-items: baseline;
+  }
+
+  .detail-card-title-no {
+    word-break: break-all;
+    line-height: 1.4;
+  }
+
+  .detail-card-title-seq {
+    flex-shrink: 0;
+    font-size: 26rpx;
+    font-weight: 500;
+    color: #888;
+    margin-left: 8rpx;
+  }
+
+  .detail-card-body {
+    padding-top: 4rpx;
+  }
+
+  .detail-card-summary {
+    padding-top: 8rpx;
+  }
+
+  .kv-row--summary {
+    padding: 12rpx 0;
+    font-size: 26rpx;
+  }
+
+  .summary-tip {
+    display: block;
+    font-size: 24rpx;
+    color: #999;
+    text-align: center;
+    padding: 20rpx 0 8rpx;
+  }
+
+  .kv-row {
+    display: flex;
+    align-items: flex-start;
+    padding: 16rpx 0;
+    border-bottom: 1rpx solid #f0f0f0;
+    font-size: 28rpx;
+  }
+
+  .kv-label {
+    flex-shrink: 0;
+    width: 220rpx;
+    color: #888;
+    line-height: 1.5;
+  }
+
+  .kv-value {
+    flex: 1;
+    color: #1a1a1a;
+    line-height: 1.5;
+    word-break: break-all;
+    text-align: right;
+  }
+
+  .kv-value--tag {
+    display: flex;
+    justify-content: flex-end;
+    align-items: center;
+  }
+
+  .stocked-qty-block {
+    margin-top: 8rpx;
+    padding-top: 8rpx;
+    border-top: 1rpx solid #f0f0f0;
+  }
+
+  .stocked-qty-row {
+    border-bottom: none;
+    align-items: center;
+  }
+
+  .stocked-qty-input-wrap {
+    min-width: 0;
   }
 
   .footer-btns {
-    margin-top: 60rpx;
     display: flex;
     justify-content: space-between;
-    padding-bottom: 40rpx;
+    align-items: center;
+    gap: 24rpx;
+    padding: 20rpx 40rpx 60rpx;
   }
 
-  .cancel-btn {
-    width: 30%;
+  .footer-cancel-btn {
+    flex: 1;
     background-color: #f5f5f5;
     color: #666;
     border: none;
   }
 
-  .save-btn {
-    width: 65%;
+  .footer-confirm-btn {
+    flex: 1;
     background: linear-gradient(140deg, #00baff 0%, #006cfb 100%);
     color: #fff;
     border: none;

--
Gitblit v1.9.3