From f0f066701bf6cf3848595cbcfcfa955343f9799d Mon Sep 17 00:00:00 2001 From: shenzhouyu Date: Fri, 18 Sep 2026 10:32:00 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=80=80=E6=96=99=E6=96=B0?= =?UTF-8?q?=E5=BB=BA=E6=A0=87=E7=AD=BE=E6=B2=A1=E6=9C=89partdesc=EF=BC=8C?= =?UTF-8?q?=E5=9C=A8shipment=E5=8F=91=E6=96=99=E6=B7=BB=E5=8A=A0=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=EF=BC=8C=E6=89=AB=E6=8F=8F=E5=BF=85=E9=A1=BB=E6=98=AF?= =?UTF-8?q?=E8=BF=99=E4=B8=AAshipment=E4=B8=8B=E7=9A=84=E7=89=A9=E6=96=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../customerIssue/customerIssuePDA.vue | 56 ++- .../customerIssuePDAIssueList.vue | 322 ++++++++++++------ .../productionReturnPDAIssueList.vue | 7 +- .../productionReturnPickingDetail.vue | 29 +- 4 files changed, 295 insertions(+), 119 deletions(-) diff --git a/src/views/modules/customerIssue/customerIssuePDA.vue b/src/views/modules/customerIssue/customerIssuePDA.vue index 00d3829..26f1bf3 100644 --- a/src/views/modules/customerIssue/customerIssuePDA.vue +++ b/src/views/modules/customerIssue/customerIssuePDA.vue @@ -275,22 +275,60 @@ export default { }, - // 打开材料列表页面(仿照 productionReturnPicking.vue 的 openIssueList) + // 打开材料列表页面:先拉取 ShipmentLine,再跳转(避免发料页每次扫描都调 IFS) openIssueDetail(material) { const site = this.getMaterialSite(material) || this.requestIssueForm.site if (!site) { this.$message.warning('未获取到申请单站点信息') return } - this.$router.push({ - name: 'customerIssuePDAIssueList', - query: { - state: material.STATE, // 申请单行号 - shipmentId: material.SHIPMENT_ID, // 客户订单号 - orderType: material.SHIPMENT_TYPE, // 订单类型 - site, - } + const shipmentId = material.SHIPMENT_ID + if (!shipmentId) { + this.$message.warning('未获取到发货单号') + return + } + if (this.loading) { + return + } + this.loading = true + this.loadingText = '加载发货单物料...' + getCustomerIssueNotifyHeaderOrderMaterialList({ + site, + workOrderNo: shipmentId, }) + .then(({ data }) => { + if (!data || data.code !== 0) { + this.$message.error((data && data.msg) || '获取发货单物料失败') + return + } + const shipmentLineList = data.data || [] + try { + sessionStorage.setItem( + `customerIssueShipmentLine_${shipmentId}`, + JSON.stringify(shipmentLineList) + ) + } catch (e) { + this.$message.error('缓存发货单物料失败,请重试') + return + } + this.$router.push({ + name: 'customerIssuePDAIssueList', + query: { + state: material.STATE, + shipmentId, + orderType: material.SHIPMENT_TYPE, + site, + }, + }) + }) + .catch((error) => { + this.$message.error( + '获取发货单物料失败: ' + ((error && error.message) || error) + ) + }) + .finally(() => { + this.loading = false + }) }, resetRequest() { diff --git a/src/views/modules/customerIssue/customerIssuePDAIssueList.vue b/src/views/modules/customerIssue/customerIssuePDAIssueList.vue index 954f823..375e314 100644 --- a/src/views/modules/customerIssue/customerIssuePDAIssueList.vue +++ b/src/views/modules/customerIssue/customerIssuePDAIssueList.vue @@ -466,10 +466,13 @@ export default { }, pendingIssueParams: null, shipmentLine: [], - // shipmentline 物料清单弹窗 + // shipmentline 物料清单(进入页面时由父页预加载并缓存) showShipmentLineDialogVisible: false, shipmentLineLoading: false, shipmentLineList: [], + shipmentLineRaw: [], // IFS ShipmentLine 原始返回,扫描校验 / 对比发料复用 + shipmentPartNoSet: {}, // partNo -> true,本地校验用 + shipmentLineInitialized: false, // 是否已完成 ShipmentLine 初始化 } }, computed: { @@ -551,6 +554,14 @@ export default { // 验证标签并添加到列表(保留客户发料功能) validateAndAddLabel(labelCode) { + if (!this.shipmentLineInitialized) { + this.$message.warning('发货单物料清单未加载,请稍候再扫') + return + } + if (!Object.keys(this.shipmentPartNoSet).length) { + this.$message.warning('该发货单暂无物料行,无法添加') + return + } const params = { scannedLabel: labelCode, workOrderNo: this.orderNo, @@ -564,16 +575,26 @@ export default { return } // 客户发料标签验证 + // 注意:后端接口会先落库再返回,不属于本 shipment 的物料必须回滚删除 scanCustomerIssueMaterialLabel(params) .then(({ data }) => { if (data.code === 0 && data) { - // 添加到列表,保存更多字段信息 - // 后端返回的是 ShipmentHuDetail 对象 - const labelInfo = data.labelInfo + const labelInfo = data.labelInfo || {} + const partNo = String( + this.pickFieldValue(labelInfo, ['partNo', 'PART_NO', 'part_no'], '') || + '' + ).trim() + if (!partNo || !this.shipmentPartNoSet[partNo]) { + this.rollbackInvalidScan(labelInfo, labelCode) + this.$message.warning( + `物料 ${partNo || '-'} 不属于当前发货单,无法添加` + ) + return + } this.labelList.push({ id: Date.now(), labelCode: labelCode, - partNo: labelInfo.partNo || '', + partNo: partNo, qtyToIssue: Number(labelInfo.qtyToIssue) || 0, batchNo: labelInfo.batchNo || '', locationId: labelInfo.locationId || '', @@ -581,7 +602,7 @@ export default { wdrNo: labelInfo.wdr || '', engChgLevel: labelInfo.engChgLevel || '1', detailId: labelInfo.detailId || '', - unitId: labelInfo.unitId || '', + unitId: labelInfo.unitId || labelCode, site: labelInfo.site || this.site, height: labelInfo.height || null, }) @@ -596,6 +617,59 @@ export default { }) }, + // 扫描接口已落库但不属于本 shipment 时,回滚删除 + rollbackInvalidScan(labelInfo, labelCode) { + const unitId = (labelInfo && (labelInfo.unitId || labelInfo.unit_id)) || labelCode + const detailId = labelInfo && labelInfo.detailId + if (!unitId) { + return + } + removeShipmentHuDetail({ + site: this.site, + unitId, + detailId, + }).catch(() => { + // 回滚失败不阻断提示,避免误导操作员 + }) + }, + + isPartBelongToShipment(partNo) { + const normalized = String(partNo || '').trim() + if (!normalized) { + return false + } + return !!this.shipmentPartNoSet[normalized] + }, + + // 过滤不属于当前 shipmentline 的历史扫描记录 + filterLabelsByShipmentLine(details = []) { + if (!this.shipmentLineInitialized || !Object.keys(this.shipmentPartNoSet).length) { + return details || [] + } + const valid = [] + const invalid = [] + ;(details || []).forEach((item) => { + const partNo = String( + this.pickFieldValue(item, ['partNo', 'PART_NO', 'part_no'], '') || '' + ).trim() + if (this.isPartBelongToShipment(partNo)) { + valid.push(item) + } else { + invalid.push(item) + } + }) + // 清理历史误扫入库的记录,避免下次进入再次出现 + invalid.forEach((item) => { + this.rollbackInvalidScan(item, item.unitId) + }) + if (invalid.length > 0) { + this.$message.warning( + `已自动移除 ${invalid.length} 条不属于当前发货单的历史扫描记录` + ) + } + return valid + }, + // 通过条码移除标签 removeLabelByCode(labelCode) { const exists = this.labelList.find((item) => item.unitId === labelCode) @@ -775,79 +849,77 @@ export default { site: this.site, workOrderNo: this.orderNo, } - await getCustomerIssueNotifyHeaderOrderMaterialList(params).then( - ({ data }) => { - if (!data || data.code !== 0) { - throw new Error((data && data.msg) || '获取申请单物料失败') + if (!materials.length) { + throw new Error('发货单物料清单为空,无法对比') + } + this.$set(this, 'shipmentLine', materials) + const labelMap = this.aggregateMaterials( + this.labelList, + ['partNo'], + ['qtyToIssue', 'quantity', 'issueQty'] + ) + // 手动聚合订单物料,计算 INVENTORY_QTY - QTY_ASSIGNED 作为需求数量 + const orderMap = {} + materials.forEach((item) => { + const partNo = this.pickFieldValue(item, ['INVENTORY_PART_NO']) + const normalizedKey = + typeof partNo === 'string' ? partNo.trim() : partNo + if (!normalizedKey) { + return + } + const inventoryQty = + Number(this.pickFieldValue(item, ['INVENTORY_QTY'], 0)) || 0 + const qtyAssigned = + Number(this.pickFieldValue(item, ['QTY_ASSIGNED'], 0)) || 0 + const requiredQty = this.safeAdd(inventoryQty, -qtyAssigned) + + if (!orderMap[normalizedKey]) { + orderMap[normalizedKey] = { + partNo: normalizedKey, + qty: 0, } - const materials = data.data || [] - this.$set(this,'shipmentLine',data.data) - const labelMap = this.aggregateMaterials( - this.labelList, - ['partNo'], - ['qtyToIssue', 'quantity', 'issueQty'] - ) - // 手动聚合订单物料,计算 INVENTORY_QTY - QTY_ASSIGNED 作为需求数量 - const orderMap = {} - materials.forEach((item) => { - const partNo = this.pickFieldValue(item, ['INVENTORY_PART_NO']) - const normalizedKey = - typeof partNo === 'string' ? partNo.trim() : partNo - if (!normalizedKey) { - return - } - const inventoryQty = Number(this.pickFieldValue(item, ['INVENTORY_QTY'], 0)) || 0 - const qtyAssigned = Number(this.pickFieldValue(item, ['QTY_ASSIGNED'], 0)) || 0 - const requiredQty = this.safeAdd(inventoryQty, -qtyAssigned) - - if (!orderMap[normalizedKey]) { - orderMap[normalizedKey] = { - partNo: normalizedKey, - qty: 0, - } - } - orderMap[normalizedKey].qty = this.safeAdd(orderMap[normalizedKey].qty, requiredQty) - }) + } + orderMap[normalizedKey].qty = this.safeAdd( + orderMap[normalizedKey].qty, + requiredQty + ) + }) - const existing = [] - const missing = [] - Object.keys(orderMap).forEach((partNo) => { - console.log('Comparing partNo:', orderMap[partNo]) - - const requiredQty = orderMap[partNo].qty - if (labelMap[partNo]) { - const scannedQty = labelMap[partNo].qty - existing.push({ - partNo, - requiredQty, - scannedQty, - difference: scannedQty - requiredQty, - }) - } else { - missing.push({ - partNo, - requiredQty, - }) - } + const existing = [] + const missing = [] + Object.keys(orderMap).forEach((partNo) => { + const requiredQty = orderMap[partNo].qty + if (labelMap[partNo]) { + const scannedQty = labelMap[partNo].qty + existing.push({ + partNo, + requiredQty, + scannedQty, + difference: scannedQty - requiredQty, }) - - const extra = [] - Object.keys(labelMap).forEach((partNo) => { - if (!orderMap[partNo]) { - extra.push({ - partNo, - scannedQty: labelMap[partNo].qty, - }) - } + } else { + missing.push({ + partNo, + requiredQty, }) + } + }) - this.comparisonData = { - existing, - missing, - extra, - } + const extra = [] + Object.keys(labelMap).forEach((partNo) => { + if (!orderMap[partNo]) { + extra.push({ + partNo, + scannedQty: labelMap[partNo].qty, + }) } - ) + }) + + this.comparisonData = { + existing, + missing, + extra, + } }, closeSummaryDialog() { if (this.loading) { @@ -864,11 +936,70 @@ export default { closeShipmentLineDialog() { this.showShipmentLineDialogVisible = false }, - async loadShipmentLineList() { + applyShipmentLineMaterials(materials = []) { + const list = Array.isArray(materials) ? materials : [] + this.shipmentLineRaw = list + this.$set(this, 'shipmentLine', list) + const partNoSet = {} + this.shipmentLineList = list.map((item) => { + const inventoryQty = + Number(this.pickFieldValue(item, ['INVENTORY_QTY'], 0)) || 0 + const qtyAssigned = + Number(this.pickFieldValue(item, ['QTY_ASSIGNED'], 0)) || 0 + const partNo = this.pickFieldValue(item, ['INVENTORY_PART_NO'], '') + const normalizedPartNo = + typeof partNo === 'string' ? partNo.trim() : partNo + if (normalizedPartNo) { + partNoSet[normalizedPartNo] = true + } + return { + lineNo: this.pickFieldValue(item, ['SHIPMENT_LINE_NO'], ''), + partNo: normalizedPartNo || '', + inventoryQty, + qtyAssigned, + remainQty: this.safeAdd(inventoryQty, -qtyAssigned), + uom: this.pickFieldValue(item, ['INVENTORY_UOM'], ''), + } + }) + this.shipmentPartNoSet = partNoSet + this.shipmentLineInitialized = true + return list + }, + // 从父页跳转时写入的 sessionStorage 初始化;失败则回退接口拉取一次 + async initShipmentLineCache() { + const cacheKey = `customerIssueShipmentLine_${this.orderNo}` + let cached = null + try { + const raw = sessionStorage.getItem(cacheKey) + if (raw) { + cached = JSON.parse(raw) + } + } catch (e) { + cached = null + } finally { + try { + sessionStorage.removeItem(cacheKey) + } catch (e) { + // ignore + } + } + if (Array.isArray(cached)) { + this.applyShipmentLineMaterials(cached) + if (!cached.length) { + this.$message.warning('该发货单暂无物料行,扫描将被禁止') + } + return + } + await this.ensureShipmentLineLoaded() + }, + async ensureShipmentLineLoaded() { + if (this.shipmentLineRaw && this.shipmentLineRaw.length > 0) { + return this.shipmentLineRaw + } if (!this.site || !this.orderNo) { this.$message.error('缺少必要参数,无法获取物料清单') - this.shipmentLineList = [] - return + this.applyShipmentLineMaterials([]) + return [] } this.shipmentLineLoading = true try { @@ -878,31 +1009,26 @@ export default { }) if (!data || data.code !== 0) { this.$message.error((data && data.msg) || '获取物料清单失败') - this.shipmentLineList = [] - return + this.applyShipmentLineMaterials([]) + return [] } - const materials = data.data || [] - this.shipmentLineList = materials.map((item) => { - const inventoryQty = - Number(this.pickFieldValue(item, ['INVENTORY_QTY'], 0)) || 0 - const qtyAssigned = - Number(this.pickFieldValue(item, ['QTY_ASSIGNED'], 0)) || 0 - return { - lineNo: this.pickFieldValue(item, ['SHIPMENT_LINE_NO'], ''), - partNo: this.pickFieldValue(item, ['INVENTORY_PART_NO'], ''), - inventoryQty, - qtyAssigned, - remainQty: this.safeAdd(inventoryQty, -qtyAssigned), - uom: this.pickFieldValue(item, ['INVENTORY_UOM'], ''), - } - }) + return this.applyShipmentLineMaterials(data.data || []) } catch (error) { this.$message.error('获取物料清单失败') - this.shipmentLineList = [] + this.applyShipmentLineMaterials([]) + return [] } finally { this.shipmentLineLoading = false } }, + async loadShipmentLineList() { + // 弹窗优先展示已缓存数据,不再每次打开都调 IFS + if (this.shipmentLineRaw && this.shipmentLineRaw.length > 0) { + this.applyShipmentLineMaterials(this.shipmentLineRaw) + return + } + await this.ensureShipmentLineLoaded() + }, proceedIssueConfirm() { if (!this.pendingIssueParams) { this.pendingIssueParams = this.buildIssueParams() @@ -1029,7 +1155,8 @@ export default { getShipmentHuDetail(params) .then(({ data }) => { if (data.code === 0 && data) { - this.labelList = data.details || [] + // 只保留属于当前 shipmentline 的扫描记录 + this.labelList = this.filterLabelsByShipmentLine(data.details || []) } else { this.$message.error(data.msg || '获取订单详情失败') } @@ -1181,7 +1308,7 @@ export default { }, }, - mounted() { + async mounted() { // 获取路由参数 console.log('路由参数:', this.$route.query) @@ -1203,7 +1330,8 @@ export default { } }) - // 加载订单详情 + // 必须先拿到 ShipmentLine,再加载历史扫描,避免错料先展示出来 + await this.initShipmentLineCache() this.loadOrderDetails() }, } diff --git a/src/views/modules/production-return/productionReturnPDAIssueList.vue b/src/views/modules/production-return/productionReturnPDAIssueList.vue index f196172..b608a41 100644 --- a/src/views/modules/production-return/productionReturnPDAIssueList.vue +++ b/src/views/modules/production-return/productionReturnPDAIssueList.vue @@ -644,6 +644,8 @@ export default { id: Date.now(), labelCode, partNo, + partDesc: (this.newLabelForm.partDesc || (this.orderInfo && this.orderInfo.description) || '').trim(), + componentPartNo: this.partNo || partNo, quantity: Number(this.newLabelForm.quantity), batchNo: this.batchNo || '', locationId: this.newLabelForm.locationId || '', @@ -733,6 +735,7 @@ export default { orderType: this.orderType, batchNo: this.batchNo, componentPartNo: this.partNo, + componentPartDesc: (this.orderInfo && this.orderInfo.description) || '', transactionId: this.transactionId, accountingId: this.accountingId, itemNo: this.itemNo, @@ -746,7 +749,9 @@ export default { labelCode: label.labelCode, issueQty: label.quantity, batchNo: label.batchNo, - partNo: label.partNo, + partNo: label.partNo || this.partNo, + partDesc: label.partDesc || (this.orderInfo && this.orderInfo.description) || '', + componentPartNo: label.componentPartNo || label.partNo || this.partNo, locationId: label.locationId, warehouseId: label.warehouseId, wdrNo: label.wdrNo || '*', diff --git a/src/views/modules/production-return/productionReturnPickingDetail.vue b/src/views/modules/production-return/productionReturnPickingDetail.vue index d637fc8..6f21927 100644 --- a/src/views/modules/production-return/productionReturnPickingDetail.vue +++ b/src/views/modules/production-return/productionReturnPickingDetail.vue @@ -611,6 +611,8 @@ export default { id: Date.now(), labelCode: labelCode, partNo: this.newLabelForm.partNo.trim(), + partDesc: (this.newLabelForm.partDesc || this.componentPartDesc || '').trim(), + componentPartNo: this.componentPartNo || this.newLabelForm.partNo.trim(), quantity: Number(this.newLabelForm.quantity), batchNo: this.batchNo || '', locationId: this.newLabelForm.locationId || '', @@ -699,6 +701,7 @@ export default { orderType: this.orderType, batchNo: this.batchNo, componentPartNo: this.componentPartNo, + componentPartDesc: this.componentPartDesc, transactionId: this.transactionId, accountingId: this.accountingId, itemNo: this.itemNo, @@ -714,7 +717,9 @@ export default { labelCode: label.labelCode, issueQty: label.quantity, batchNo: label.batchNo, - partNo: label.partNo, + partNo: label.partNo || this.componentPartNo, + partDesc: label.partDesc || this.componentPartDesc || '', + componentPartNo: label.componentPartNo || label.partNo || this.componentPartNo, locationId: label.locationId, warehouseId: label.warehouseId, wdrNo: label.wdrNo || '*', @@ -1158,10 +1163,10 @@ export default { .title-label { font-size: 11px; - color: #999; + color: #333; display: block; margin-bottom: 6px; - font-weight: normal; + font-weight: 500; } .title-value { @@ -1177,13 +1182,13 @@ export default { justify-content: space-between; align-items: flex-start; gap: 4px; + width: 100%; } .detail-item { flex: 1; text-align: center; - min-width: 60px; - max-width: 60px; + min-width: 0; } .detail-label { @@ -1192,7 +1197,7 @@ export default { margin-bottom: 6px; font-weight: normal; line-height: 1.2; - margin-left: -12px; + margin-left: 0; } .detail-value { @@ -1241,7 +1246,7 @@ export default { .work-order-card { background: white; border-radius: 8px; - padding: 16px; + padding: 10px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); transition: all 0.2s ease; border: 2px solid transparent; @@ -2059,14 +2064,14 @@ export default { } .card-details { - flex-wrap: wrap; - gap: 6px; + flex-wrap: nowrap; + gap: 8px; } .detail-item { - flex: 0 0 48%; - margin-bottom: 6px; - min-width: 50px; + flex: 1; + margin-bottom: 0; + min-width: 0; } .list-header,