| | |
| | | <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 |
| | |
| | | </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> |
| | |
| | | @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"; |
| | |
| | | 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(() => { |
| | |
| | | }; |
| | | }; |
| | | |
| | | 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 "-"; |
| | |
| | | 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") { |
| | |
| | | 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") { |
| | |
| | | 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); |
| | |
| | | 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("提交中..."); |
| | |
| | | } finally { |
| | | submitLoading.value = false; |
| | | } |
| | | }; |
| | | |
| | | const confirmInbound = () => { |
| | | if (!validateApproverNodes()) return; |
| | | submitInbound(); |
| | | }; |
| | | |
| | | const goBack = () => { |
| | |
| | | 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; |
| | |
| | | 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> |