yyb
2 天以前 2fb38b2d2d005c0ff524b619a529fee1ca151095
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,24 +274,33 @@
    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 formatCell = (item, row, idx) => {
@@ -242,11 +314,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,9 +363,65 @@
    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;
@@ -286,7 +438,11 @@
      return;
    }
    const runApi = currentSubmitConfig.runApi;
    const payload = currentSubmitConfig.payloadBuilder(salesLedgerProductList);
    const inboundApproveUserIds = stockApproverNodes.value.map(node => node.userId).join(",");
    const payload = currentSubmitConfig.payloadBuilder(
      salesLedgerProductList,
      inboundApproveUserIds
    );
    try {
      submitLoading.value = true;
      modal.loading("提交中...");
@@ -304,6 +460,11 @@
    } finally {
      submitLoading.value = false;
    }
  };
  const confirmInbound = () => {
    if (!validateApproverNodes()) return;
    submitInbound();
  };
  const goBack = () => {
@@ -351,7 +512,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 +751,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>