| | |
| | | |
| | | <view class="scan-container"> |
| | | |
| | | <PageHeader title="扫码出库" |
| | | <PageHeader title="扫码发货" |
| | | |
| | | @back="goBack" /> |
| | | |
| | |
| | | |
| | | <view class="module-info"> |
| | | |
| | | <text class="module-label">合格出库</text> |
| | | <text class="module-label">合格发货</text> |
| | | |
| | | <text class="module-desc">扫描合格品进行领用出库</text> |
| | | <text class="module-desc">扫描销售订单,填写发货数量、车牌与审批人后提交发货审批(通过后自动发货)</text> |
| | | |
| | | </view> |
| | | |
| | |
| | | |
| | | <view class="kv-row stocked-qty-row"> |
| | | |
| | | <text class="kv-label">出库数量</text> |
| | | <text class="kv-label">{{ needScanShipFlow ? "本次发货数量" : "出库数量" }}</text> |
| | | |
| | | <view class="kv-value stocked-qty-input-wrap"> |
| | | |
| | | <up-input :key="'stocked-' + idx" |
| | | <text v-if="needScanShipFlow" |
| | | |
| | | class="scan-ship-qty-readonly">{{ resolveScanShipLineQuantity(item) }}</text> |
| | | |
| | | <up-input v-else |
| | | |
| | | :key="'stocked-' + idx" |
| | | |
| | | v-model="item.operateQuantity" |
| | | |
| | | type="number" |
| | | |
| | | placeholder="请输入出库数量" |
| | | :clearable="!isSalesQualifiedOutboundQtyLocked" |
| | | |
| | | clearable |
| | | :disabled="isSalesQualifiedOutboundQtyLocked" |
| | | |
| | | |
| | | border="surround" |
| | | |
| | |
| | | |
| | | </view> |
| | | |
| | | <text v-if="needScanShipFlow" |
| | | |
| | | class="scan-ship-qty-hint">整单一次性发货,数量以订单为准,不可修改</text> |
| | | |
| | | </view> |
| | | |
| | | </view> |
| | | |
| | | <view v-if="needScanShipFlow" |
| | | |
| | | class="scan-ship-card"> |
| | | |
| | | <view class="scan-ship-title">发货信息</view> |
| | | |
| | | <u-form label-width="160rpx"> |
| | | |
| | | <u-form-item label="发货方式" |
| | | |
| | | required> |
| | | |
| | | <u-input v-model="scanShipTypeLabel" |
| | | |
| | | readonly |
| | | |
| | | placeholder="请选择" |
| | | |
| | | @click="scanShipTypeSheetShow = true" /> |
| | | |
| | | </u-form-item> |
| | | |
| | | <u-form-item v-if="scanShipTypeValue === '货车'" |
| | | |
| | | label="车牌号" |
| | | |
| | | required> |
| | | |
| | | <u-input v-model="scanShipCarNumber" |
| | | |
| | | placeholder="请输入车牌号" |
| | | |
| | | clearable /> |
| | | |
| | | </u-form-item> |
| | | |
| | | <u-form-item v-if="scanShipTypeValue === '快递'" |
| | | |
| | | label="快递单号" |
| | | |
| | | required> |
| | | |
| | | <u-input v-model="scanShipExpress" |
| | | |
| | | placeholder="请输入快递单号" |
| | | |
| | | clearable /> |
| | | |
| | | </u-form-item> |
| | | |
| | | </u-form> |
| | | |
| | | <view class="scan-ship-approval"> |
| | | |
| | | <text class="scan-ship-subtitle">审批人</text> |
| | | |
| | | <view v-if="scanShipApprover.nickName" |
| | | |
| | | class="scan-ship-approver-pill"> |
| | | |
| | | <text>{{ scanShipApprover.nickName }}</text> |
| | | |
| | | <text class="scan-ship-remove" |
| | | |
| | | @click="clearScanShipApprover">×</text> |
| | | |
| | | </view> |
| | | |
| | | <u-button v-else |
| | | |
| | | size="small" |
| | | |
| | | type="primary" |
| | | |
| | | plain |
| | | |
| | | @click="openScanShipContactSelect">选择审批人</u-button> |
| | | |
| | | </view> |
| | | |
| | | <view class="scan-ship-files"> |
| | | |
| | | <text class="scan-ship-subtitle">发货附件(选填,最多10张)</text> |
| | | |
| | | <u-button size="small" |
| | | |
| | | type="primary" |
| | | |
| | | :loading="scanShipUploading" |
| | | |
| | | :disabled="scanShipFiles.length >= 10" |
| | | |
| | | @click="chooseScanShipImage">添加图片</u-button> |
| | | |
| | | <view v-if="scanShipFiles.length" |
| | | |
| | | class="scan-ship-file-grid"> |
| | | |
| | | <view v-for="(f, fi) in scanShipFiles" |
| | | |
| | | :key="fi" |
| | | |
| | | class="scan-ship-thumb-wrap"> |
| | | |
| | | <image :src="scanShipFileUrl(f)" |
| | | |
| | | class="scan-ship-thumb" |
| | | |
| | | mode="aspectFill" |
| | | |
| | | @click="previewScanShipImage(fi)" /> |
| | | |
| | | <text class="scan-ship-thumb-del" |
| | | |
| | | @click.stop="removeScanShipFile(fi)">×</text> |
| | | |
| | | </view> |
| | | |
| | | </view> |
| | | |
| | | </view> |
| | | |
| | | </view> |
| | | |
| | | <up-action-sheet :show="scanShipTypeSheetShow" |
| | | |
| | | :actions="scanShipTypeActions" |
| | | |
| | | title="发货方式" |
| | | |
| | | @select="onScanShipTypeSelect" |
| | | |
| | | @close="scanShipTypeSheetShow = false" /> |
| | | |
| | | <view class="footer-btns"> |
| | | |
| | |
| | | |
| | | @click="cancelForm">返回</u-button> |
| | | |
| | | <u-button class="footer-confirm-btn" |
| | | <u-button v-if="needScanShipFlow" |
| | | |
| | | class="footer-confirm-btn" |
| | | |
| | | :loading="submitLoading" |
| | | |
| | | @click="confirmScanShipApply">提交发货审批</u-button> |
| | | |
| | | <u-button v-else |
| | | |
| | | class="footer-confirm-btn" |
| | | |
| | | :loading="submitLoading" |
| | | |
| | |
| | | |
| | | <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 { productList as salesProductList, scanShipApply } from "@/api/salesManagement/salesLedger"; |
| | | |
| | | import config from "@/config"; |
| | | import { getToken } from "@/utils/auth"; |
| | | import { getLedgerShippingLabel } from "@/utils/salesLedgerShip"; |
| | | |
| | | import modal from "@/plugins/modal"; |
| | | |
| | |
| | | resolveListTypeForDetail, |
| | | resolveContractNo, |
| | | buildSalesLedgerProductList, |
| | | buildScanShipProductList, |
| | | resolveScanShipLineQuantity, |
| | | hasAnyPositiveStockedQty, |
| | | resolveSubmitSceneKey, |
| | | } from "./scanOut.logic"; |
| | |
| | | |
| | | const submitLoading = ref(false); |
| | | |
| | | /** 销售 + 合格:走「提交发货审批」,不再直接出库或跳转原发货页 */ |
| | | const needScanShipFlow = computed( |
| | | () => |
| | | showForm.value && |
| | | type.value === QUALITY_TYPE.qualified && |
| | | contractKind.value === CONTRACT_KIND.sales |
| | | ); |
| | | |
| | | const scanShipTypeValue = ref("货车"); |
| | | const scanShipTypeLabel = computed(() => scanShipTypeValue.value); |
| | | const scanShipTypeSheetShow = ref(false); |
| | | const scanShipTypeActions = ref([ |
| | | { name: "货车", value: "货车" }, |
| | | { name: "快递", value: "快递" }, |
| | | ]); |
| | | const scanShipCarNumber = ref(""); |
| | | const scanShipExpress = ref(""); |
| | | const scanShipApprover = ref({ userId: null, nickName: null }); |
| | | const scanShipFiles = ref([]); |
| | | const scanShipUploading = ref(false); |
| | | |
| | | const scanShipUploadConfig = { |
| | | action: "/file/upload", |
| | | limit: 10, |
| | | fileType: ["jpg", "jpeg", "png", "gif", "webp"], |
| | | formType: 10, |
| | | }; |
| | | |
| | | const uploadScanShipFileUrl = computed( |
| | | () => (config.baseUrl || "").replace(/\/$/, "") + scanShipUploadConfig.action |
| | | ); |
| | | |
| | | const onScanShipTypeSelect = item => { |
| | | scanShipTypeValue.value = item.name || item.value || "货车"; |
| | | scanShipTypeSheetShow.value = false; |
| | | if (scanShipTypeValue.value === "货车") scanShipExpress.value = ""; |
| | | else scanShipCarNumber.value = ""; |
| | | }; |
| | | |
| | | const openScanShipContactSelect = () => { |
| | | uni.setStorageSync("stepIndex", 0); |
| | | uni.navigateTo({ |
| | | url: "/pages/cooperativeOffice/collaborativeApproval/contactSelect?approveType=7&source=scanShip", |
| | | }); |
| | | }; |
| | | |
| | | const clearScanShipApprover = () => { |
| | | scanShipApprover.value = { userId: null, nickName: null }; |
| | | }; |
| | | |
| | | const handleScanShipSelectContact = data => { |
| | | if (data?.source !== "scanShip") return; |
| | | if (!needScanShipFlow.value) return; |
| | | const c = data?.contact; |
| | | if (!c) return; |
| | | scanShipApprover.value = { userId: c.userId, nickName: c.nickName }; |
| | | }; |
| | | |
| | | const scanShipFileUrl = file => { |
| | | const base = config.fileUrl || ""; |
| | | const link = file?.link || file?.url || ""; |
| | | if (link && String(link).startsWith("http")) return link; |
| | | if (link && link.startsWith("/")) return base + link; |
| | | return file?.tempFilePath || file?.path || ""; |
| | | }; |
| | | |
| | | const chooseScanShipImage = () => { |
| | | if (scanShipFiles.value.length >= scanShipUploadConfig.limit) { |
| | | modal.msgError(`最多${scanShipUploadConfig.limit}张`); |
| | | return; |
| | | } |
| | | uni.chooseImage({ |
| | | count: 1, |
| | | sizeType: ["compressed"], |
| | | sourceType: ["album", "camera"], |
| | | success: res => { |
| | | const path = res?.tempFilePaths?.[0]; |
| | | const tf = res?.tempFiles?.[0] || {}; |
| | | if (!path) return; |
| | | const token = getToken(); |
| | | if (!token) { |
| | | modal.msgError("未登录"); |
| | | return; |
| | | } |
| | | scanShipUploading.value = true; |
| | | uni.uploadFile({ |
| | | url: uploadScanShipFileUrl.value, |
| | | filePath: path, |
| | | name: "file", |
| | | formData: { type: scanShipUploadConfig.formType }, |
| | | header: { Authorization: `Bearer ${token}` }, |
| | | success: up => { |
| | | try { |
| | | const body = JSON.parse(up.data || "{}"); |
| | | if (body.code === 200 && body.data) { |
| | | const d = body.data; |
| | | scanShipFiles.value.push({ |
| | | tempId: d.tempId ?? d.tempFileId ?? d.id, |
| | | link: d.link || d.url, |
| | | url: d.url, |
| | | name: d.originalFilename || d.originalName || "图片", |
| | | tempFilePath: path, |
| | | }); |
| | | modal.msgSuccess("上传成功"); |
| | | } else { |
| | | modal.msgError(body.msg || "上传失败"); |
| | | } |
| | | } catch (e) { |
| | | modal.msgError("上传解析失败"); |
| | | } |
| | | }, |
| | | fail: () => modal.msgError("上传失败"), |
| | | complete: () => { |
| | | scanShipUploading.value = false; |
| | | }, |
| | | }); |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | const removeScanShipFile = idx => { |
| | | scanShipFiles.value.splice(idx, 1); |
| | | }; |
| | | |
| | | const previewScanShipImage = idx => { |
| | | const urls = scanShipFiles.value.map(f => scanShipFileUrl(f)).filter(Boolean); |
| | | const cur = urls[idx]; |
| | | if (urls.length && cur) uni.previewImage({ urls, current: cur }); |
| | | }; |
| | | |
| | | const getScanShipTempFileIds = () => |
| | | scanShipFiles.value.map(f => f.tempId).filter(id => id != null && id !== ""); |
| | | |
| | | const confirmScanShipApply = async () => { |
| | | if (scanLedgerId.value == null || scanLedgerId.value === "") { |
| | | modal.msgError("缺少订单信息,请重新扫码"); |
| | | return; |
| | | } |
| | | if (!hasEditableOutboundItems.value) { |
| | | modal.msgError("该产品已经全部出库"); |
| | | return; |
| | | } |
| | | const salesLedgerProductList = buildScanShipProductList(recordList.value); |
| | | if (!hasAnyPositiveStockedQty(salesLedgerProductList)) { |
| | | modal.msgError("当前订单无可发货数量"); |
| | | return; |
| | | } |
| | | if (scanShipTypeValue.value === "货车" && !String(scanShipCarNumber.value || "").trim()) { |
| | | modal.msgError("请输入车牌号"); |
| | | return; |
| | | } |
| | | if (scanShipTypeValue.value === "快递" && !String(scanShipExpress.value || "").trim()) { |
| | | modal.msgError("请输入快递单号"); |
| | | return; |
| | | } |
| | | if (!scanShipApprover.value?.userId) { |
| | | modal.msgError("请选择审批人"); |
| | | return; |
| | | } |
| | | if (scanShipUploading.value) { |
| | | modal.msgError("附件上传中,请稍候"); |
| | | return; |
| | | } |
| | | const payload = { |
| | | salesLedgerId: scanLedgerId.value, |
| | | salesLedgerProductList, |
| | | approveUserIds: String(scanShipApprover.value.userId), |
| | | shipType: scanShipTypeValue.value, |
| | | shippingCarNumber: |
| | | scanShipTypeValue.value === "货车" ? String(scanShipCarNumber.value || "").trim() : "", |
| | | expressNumber: |
| | | scanShipTypeValue.value === "快递" ? String(scanShipExpress.value || "").trim() : "", |
| | | tempFileIds: getScanShipTempFileIds(), |
| | | }; |
| | | try { |
| | | submitLoading.value = true; |
| | | modal.loading("提交中..."); |
| | | const res = await scanShipApply(payload); |
| | | modal.closeLoading(); |
| | | if (res.code === 200) { |
| | | modal.msgSuccess(res.msg || "发货审批已发起"); |
| | | resetDetailView(); |
| | | } else { |
| | | modal.msgError(res.msg || "提交失败"); |
| | | } |
| | | } catch (e) { |
| | | modal.closeLoading(); |
| | | console.error(e); |
| | | } finally { |
| | | submitLoading.value = false; |
| | | } |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | uni.$on("selectContact", handleScanShipSelectContact); |
| | | }); |
| | | onUnmounted(() => { |
| | | uni.$off("selectContact", handleScanShipSelectContact); |
| | | }); |
| | | const isSalesQualifiedOutboundQtyLocked = computed( |
| | | () => |
| | | type.value === QUALITY_TYPE.qualified && |
| | | contractKind.value === CONTRACT_KIND.sales |
| | | ); |
| | | const submitConfigByScene = createSubmitConfig(scanLedgerId); |
| | | |
| | | const cardTitleMain = computed(() => { |
| | |
| | | |
| | | return formatProductStockStatus(item.productStockStatus); |
| | | |
| | | if (row.key === "ledgerShippingStatus") return getLedgerShippingLabel(item); |
| | | |
| | | if (row.key === "heavyBox") return formatHeavyBox(item.heavyBox); |
| | | |
| | | if (row.key === "remainingQuantity") { |
| | |
| | | expandedByIndex.value = {}; |
| | | |
| | | recordList.value = []; |
| | | |
| | | scanShipTypeValue.value = "货车"; |
| | | scanShipCarNumber.value = ""; |
| | | scanShipExpress.value = ""; |
| | | scanShipApprover.value = { userId: null, nickName: null }; |
| | | scanShipFiles.value = []; |
| | | scanShipUploading.value = false; |
| | | scanShipTypeSheetShow.value = false; |
| | | |
| | | }; |
| | | |
| | |
| | | } |
| | | |
| | | }; |
| | | |
| | | |
| | | |
| | | const goBack = () => { |
| | | |
| | |
| | | |
| | | } |
| | | |
| | | .scan-ship-qty-readonly { |
| | | |
| | | font-size: 30rpx; |
| | | |
| | | font-weight: 600; |
| | | |
| | | color: #1a1a1a; |
| | | |
| | | } |
| | | |
| | | .scan-ship-qty-hint { |
| | | |
| | | display: block; |
| | | |
| | | font-size: 24rpx; |
| | | |
| | | color: #999; |
| | | |
| | | padding: 8rpx 0 0; |
| | | |
| | | line-height: 1.4; |
| | | |
| | | } |
| | | |
| | | |
| | | |
| | | .footer-btns { |