From c395f33a2b0503bd8db8ec52d8adba16a2c6cd4f Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Wed, 30 Sep 2026 14:53:47 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/return-receipt.js | 19 +++- src/pages/return-receipt/return-receipt.vue | 101 ++++++++++++++++---- src/stores/warehouse-work.js | 4 +- tests/return-receipt-page.test.mjs | 8 +- 4 files changed, 107 insertions(+), 25 deletions(-) diff --git a/src/api/return-receipt.js b/src/api/return-receipt.js index 3698ddf..35ba2ae 100644 --- a/src/api/return-receipt.js +++ b/src/api/return-receipt.js @@ -47,7 +47,7 @@ export function normalizeReturnSkuCandidates(value = {}) { } export function normalizeReturnReceiptResult(value = {}) { - return { + const result = { returnNo: String(value.returnNo || ''), receiptNo: String(value.receiptNo || ''), receiptBatchNo: String(value.receiptBatchNo || ''), @@ -58,6 +58,17 @@ export function normalizeReturnReceiptResult(value = {}) { receivedQuantity: count(value.receivedQuantity), signedAt: value.signedAt || null } + if (value.cartonMark != null || value.printableHtml != null || value.totalBoxCount != null) { + result.cartonMark = String(value.cartonMark || '') + result.printableHtml = String(value.printableHtml || '') + result.totalBoxCount = count(value.totalBoxCount) + } + if (value.unitType != null || value.unitCode != null) { + result.unitType = String(value.unitType || (value.palletCode ? 'PALLET' : '')) + result.unitCode = String(value.unitCode || value.palletCode || '') + } + if (Array.isArray(value.labels)) result.labels = value.labels.map((item) => ({ cartonMark: String(item.cartonMark || ''), printableHtml: String(item.printableHtml || '') })).filter((item) => item.cartonMark && item.printableHtml) + return result } export async function startReturnReceipt(returnNo) { @@ -80,7 +91,11 @@ export async function findReturnSkuCandidates(skuCode) { } export async function confirmReturnReceipt(request) { - return normalizeReturnReceiptResult(await overseasRequest('POST', `${BASE}/confirm`, request)) + const value = String(request?.palletCode || '') + const encoded = value.startsWith('BOX:') || value.startsWith('PARCEL:') + ? { ...request, unitType: value.slice(0, value.indexOf(':')), unitCode: value.slice(value.indexOf(':') + 1), palletCode: undefined } + : request + return normalizeReturnReceiptResult(await overseasRequest('POST', `${BASE}/confirm`, encoded)) } export async function createReturnPallet(returnNo, skuCode) { diff --git a/src/pages/return-receipt/return-receipt.vue b/src/pages/return-receipt/return-receipt.vue index c9c1eb6..3f41748 100644 --- a/src/pages/return-receipt/return-receipt.vue +++ b/src/pages/return-receipt/return-receipt.vue @@ -2,38 +2,45 @@ - + 扫描 SKU 69 码 扫描完成后自动匹配对应退货单 - + 匹配退货单{{ candidates.length }} 个候选 {{ entryMode === 'SKU' ? '当前绑定仓没有包含该 SKU 的待签收退货单' : '当前绑定仓没有匹配的待签收或签收中退货单' }} - @@ -43,51 +50,104 @@ import ScanTargetInput from '@/components/ScanTargetInput.vue' import { checkReturnPallet, confirmReturnReceipt, createReturnPallet, downloadReturnPalletPhoto, findReturnSkuCandidates, manuallyCompleteReturnReceipt, startReturnReceipt, uploadReturnPalletPhoto } from '@/api/return-receipt.js' import { session, requireSession } from '@/stores/session.js' - import { releaseWarehouseKey, resetWarehouseDomain, restoreWarehouseWork, updateWarehouseWork, warehouseIdempotencyKey, warehouseWork } from '@/stores/warehouse-work.js' + import { resetWarehouseDomain, restoreWarehouseWork, updateWarehouseWork, warehouseIdempotencyKey, warehouseWork } from '@/stores/warehouse-work.js' import { startScanBroadcast } from '@/utils/scan-broadcast.js' import { playErrorSound, playSuccessSound } from '@/utils/audio-feedback.js' export default { components: { PortalHeader, ScanTargetInput }, - data() { return { user: session.currentUser, current: null, candidates: null, result: null, selectedSku: '', palletCheck: null, newPalletHtml: '', photoFileIds: [], photoNames: [], photoUrls: {}, activeTarget: 'lookup', busy: false, error: null } }, + data() { return { user: session.currentUser, current: null, candidates: null, result: null, selectedSku: '', palletCheck: null, newPalletHtml: '', unitType: 'PALLET', unitCode: '', boxRows: [{ items: {} }], palletLength: '', palletWidth: '', palletHeight: '', palletWeight: '', unitOptions: [{ value: 'PALLET', label: '托盘' }, { value: 'BOX', label: '箱子' }, { value: 'PARCEL', label: '包裹' }], photoFileIds: [], photoNames: [], photoUrls: {}, activeTarget: 'lookup', busy: false, error: null } }, computed: { + receiptBatch() { return warehouseWork.returnReceipt.pendingBatch }, entryMode: { get() { return warehouseWork.returnReceipt.entryMode }, set(value) { updateWarehouseWork('returnReceipt', { entryMode: value }) } }, lookupCode: { get() { return warehouseWork.returnReceipt.lookupCode }, set(value) { updateWarehouseWork('returnReceipt', { lookupCode: value }) } }, startedBy: { get() { return warehouseWork.returnReceipt.startedBy }, set(value) { updateWarehouseWork('returnReceipt', { startedBy: value }) } }, trackingNo: { get() { return warehouseWork.returnReceipt.trackingNo }, set(value) { updateWarehouseWork('returnReceipt', { trackingNo: value }) } }, palletCode: { get() { return warehouseWork.returnReceipt.palletCode }, set(value) { updateWarehouseWork('returnReceipt', { palletCode: value }) } }, selectedItems() { return this.current?.expectedItems?.filter(item => !this.selectedSku || item.skuCode === this.selectedSku) || [] }, + palletBoxCount() { return this.selectedItems.reduce((sum, item) => sum + this.boxCount(item.skuCode), 0) }, activeSkuCode() { return this.selectedItems.length === 1 ? this.selectedItems[0].skuCode : '' }, - receiptQuantity() { return Object.values(warehouseWork.returnReceipt.quantities || {}).reduce((total, value) => total + (Number(value) || 0), 0) }, - canConfirm() { return this.current && this.palletCode.trim() && this.palletCheck && this.selectedItems.length === 1 && this.receiptQuantity > 0 && this.selectedItems.every((item) => this.quantity(item.skuCode) <= this.remaining(item)) } + receiptQuantity() { return this.unitType === 'BOX' ? this.boxRows.reduce((sum, box) => sum + Object.values(box.items).reduce((n, quantity) => n + (Number(quantity) || 0), 0), 0) : Object.values(warehouseWork.returnReceipt.quantities || {}).reduce((total, value) => total + (Number(value) || 0), 0) }, + canConfirm() { if (!this.current || !this.selectedItems.length || this.receiptQuantity <= 0) return false; if (this.unitType === 'BOX') return this.boxRows.length > 0 && this.boxRows.every(box => Object.values(box.items).some(quantity => Number(quantity) > 0)) && this.selectedItems.every(item => this.boxRows.reduce((sum, box) => sum + (Number(box.items[item.skuCode]) || 0), 0) <= this.remaining(item)); return (this.unitType === 'PARCEL' ? this.unitCode.trim() : this.palletCode.trim() && this.palletCheck && Number(this.palletLength) > 0 && Number(this.palletWidth) > 0 && Number(this.palletHeight) > 0 && Number(this.palletWeight) > 0 && (this.unitType !== 'PALLET' || this.palletBoxCount > 0)) && this.selectedItems.every((item) => this.quantity(item.skuCode) <= this.remaining(item) && (this.unitType !== 'PALLET' || this.quantity(item.skuCode) === 0 || this.boxCount(item.skuCode) > 0)); }, + unitLabel() { return this.unitType === 'PALLET' ? '托盘码' : this.unitType === 'BOX' ? '箱号/箱唛' : '包裹码/快递单号' }, + unitPlaceholder() { return this.unitType === 'PALLET' ? '扫描托盘标签' : this.unitType === 'BOX' ? '扫描箱唛或录入箱号' : '扫描包裹面单或录入包裹码' } + ,palletVolume() { const l = Number(this.palletLength) || 0; const w = Number(this.palletWidth) || 0; const h = Number(this.palletHeight) || 0; return l > 0 && w > 0 && h > 0 ? (l * w * h).toFixed(2) : '—' } }, - onShow() { if (!requireSession()) return; this.user = session.currentUser; restoreWarehouseWork(`${this.user.id}:${this.user.warehouseId}`); this.entryMode = 'SKU'; this.startScanner(); if (warehouseWork.returnReceipt.currentReturnNo && !this.current && !this.busy) this.openOrder(warehouseWork.returnReceipt.currentReturnNo, 'SKU', false) }, + onShow() { if (!requireSession()) return; this.user = session.currentUser; restoreWarehouseWork(`${this.user.id}:${this.user.warehouseId}`); this.entryMode = 'SKU'; this.startScanner(); if (!this.receiptBatch && warehouseWork.returnReceipt.currentReturnNo && !this.current && !this.busy) this.openOrder(warehouseWork.returnReceipt.currentReturnNo, 'SKU', false) }, onHide() { this.stopScanner() }, onUnload() { this.stopScanner() }, beforeUnmount() { this.stopScanner() }, methods: { + selectUnitType(type) { if (this.busy) return; this.unitType = type; this.unitCode = ''; this.boxRows = [{ items: {} }]; this.palletCheck = null; this.activeTarget = type === 'PALLET' ? 'pallet' : 'unit' }, + resizeBoxRows(raw) { const count = Math.floor(Number(raw) || 0); if (count < 1 || count > 100) return; this.boxRows = Array.from({ length: count }, (_, index) => this.boxRows[index] || { items: {} }) }, + setBoxQuantity(index, skuCode, raw) { const value = Math.max(0, Math.floor(Number(raw) || 0)); this.boxRows[index].items = { ...this.boxRows[index].items, [skuCode]: value } }, + acceptUnit(raw) { const code = String(raw?.detail?.value ?? raw ?? '').trim(); if (!code) return; this.unitCode = code; this.palletCode = `${this.unitType}:${code}`; this.activeTarget = 'tracking'; playSuccessSound() }, startScanner() { this.stopScanner(); this._stopScanner = startScanBroadcast({ onScan: (value) => this.handleScan(value), onError: (error) => this.setError(error, '扫码广播注册失败') }) }, stopScanner() { if (this._stopScanner) this._stopScanner(); this._stopScanner = null }, - handleScan(value) { if (this.busy) return; if (!this.current) { this.lookupCode = value; this.lookup(value); return } if (this.activeTarget === 'pallet') this.acceptPallet(value); else this.acceptTracking(value) }, + handleScan(value) { if (this.busy || this.receiptBatch) return; if (!this.current) { this.lookupCode = value; this.lookup(value); return } if (this.activeTarget === 'unit') this.acceptUnit(value); else this.acceptTracking(value) }, switchMode(mode) { if (this.busy) return; this.entryMode = mode; this.lookupCode = ''; this.candidates = null; this.error = null; this.activeTarget = 'lookup' }, async lookup(raw) { const value = String(raw?.detail?.value ?? raw ?? this.lookupCode).trim(); if (!value || this.busy) return; this.lookupCode = value; this.error = null; this.result = null; if (this.entryMode === 'SKU') await this.run(async () => { const response = await findReturnSkuCandidates(value); this.candidates = response.candidates; this.selectedSku = response.skuCode; if (this.candidates.length === 1) await this.openOrderValue(this.candidates[0].returnNo, 'SKU'); else if (this.candidates.length) playSuccessSound() }, '查询 SKU 候选失败'); else await this.openOrder(value, 'RETURN_NO') }, async selectCandidate(item) { if (!item?.returnNo || this.busy) return; this.selectedSku = this.lookupCode; await this.openOrder(item.returnNo, 'SKU') }, async openOrder(returnNo, startedBy, feedback = true) { await this.run(async () => { await this.openOrderValue(returnNo, startedBy, feedback) }, '读取退货单失败') }, - async openOrderValue(returnNo, startedBy, feedback = true) { const value = await startReturnReceipt(returnNo); if (!value) { this.current = null; this.candidates = []; updateWarehouseWork('returnReceipt', { currentReturnNo: '', quantities: {} }); this.activeTarget = 'lookup'; return } this.current = value; this.palletCheck = null; this.candidates = null; this.startedBy = startedBy; this.photoFileIds = (value.photos || []).map((item) => String(item.fileId)); this.photoNames = (value.photos || []).map((item) => item.fileName || '退货打托照片'); this.photoUrls = {}; await Promise.all(this.photoFileIds.map(async (fileId) => { try { this.photoUrls[fileId] = await downloadReturnPalletPhoto(value.returnNo, fileId) } catch (_) { /* 单张照片失败不阻断作业 */ } })); updateWarehouseWork('returnReceipt', { currentReturnNo: value.returnNo, startedBy, lookupCode: value.returnNo, palletCode: '', quantities: this.validQuantities(value) }); this.activeTarget = 'pallet'; if (feedback) playSuccessSound() }, + async openOrderValue(returnNo, startedBy, feedback = true) { const value = await startReturnReceipt(returnNo); if (!value) { this.current = null; this.candidates = []; updateWarehouseWork('returnReceipt', { currentReturnNo: '', quantities: {}, boxCounts: {} }); this.activeTarget = 'lookup'; return } this.current = value; this.palletCheck = null; this.candidates = null; this.startedBy = startedBy; this.photoFileIds = (value.photos || []).map((item) => String(item.fileId)); this.photoNames = (value.photos || []).map((item) => item.fileName || '退货打托照片'); this.photoUrls = {}; await Promise.all(this.photoFileIds.map(async (fileId) => { try { this.photoUrls[fileId] = await downloadReturnPalletPhoto(value.returnNo, fileId) } catch (_) { /* 单张照片失败不阻断作业 */ } })); updateWarehouseWork('returnReceipt', { currentReturnNo: value.returnNo, startedBy, lookupCode: value.returnNo, palletCode: '', quantities: this.validQuantities(value), boxCounts: {} }); this.activeTarget = 'pallet'; if (feedback) playSuccessSound() }, validQuantities(order) { const source = warehouseWork.returnReceipt.quantities || {}; return Object.fromEntries(order.expectedItems.map((item) => [item.skuCode, Math.min(Math.max(0, Number(source[item.skuCode]) || 0), this.remaining(item))])) }, quantity(skuCode) { return Math.max(0, Number(warehouseWork.returnReceipt.quantities?.[skuCode]) || 0) }, + boxCount(skuCode) { return Math.max(0, Number(warehouseWork.returnReceipt.boxCounts?.[skuCode]) || 0) }, remaining(item) { return Math.max(0, Number(item.expectedQuantity || 0) - Number(item.receivedQuantity || 0)) }, setQuantity(item, raw) { const maximum = this.remaining(item); const value = Math.min(maximum, Math.max(0, Math.floor(Number(raw) || 0))); updateWarehouseWork('returnReceipt', { quantities: { ...(warehouseWork.returnReceipt.quantities || {}), [item.skuCode]: value } }) }, + setBoxCount(item, raw) { const value = Math.max(0, Math.floor(Number(raw) || 0)); updateWarehouseWork('returnReceipt', { boxCounts: { ...(warehouseWork.returnReceipt.boxCounts || {}), [item.skuCode]: value } }) }, fillRemaining() { updateWarehouseWork('returnReceipt', { quantities: Object.fromEntries(this.selectedItems.map((item) => [item.skuCode, this.remaining(item)])) }) }, async acceptPallet(raw) { const code = String(raw?.detail?.value ?? raw ?? '').trim(); if (!code || this.busy) return; if (!this.activeSkuCode) { this.setError(new Error('请先通过 SKU 69 码选择一个待签收 SKU'), '托盘校验失败'); return } await this.run(async () => { const value = await checkReturnPallet(this.current.returnNo, code); this.palletCode = value.palletCode; this.newPalletHtml = ''; this.palletCheck = value; this.activeTarget = 'tracking'; playSuccessSound() }, '托盘校验失败') }, async generatePallet() { if (!this.current || this.busy) return; const skuCode = this.activeSkuCode; if (!skuCode) { this.setError(new Error('请先通过 SKU 69 码选择一个待签收 SKU'), '无法生成退货托盘'); return } await this.run(async () => { const value = await createReturnPallet(this.current.returnNo, skuCode); if (!value.palletCode) throw new Error('服务端未返回新托盘码'); this.palletCode = value.palletCode; this.newPalletHtml = value.printableHtml; this.palletCheck = { palletCode: value.palletCode, empty: true, skuCodes: [], quantity: 0 }; this.activeTarget = 'tracking'; playSuccessSound() }, '生成退货托盘失败') }, printPallet() { if (!this.newPalletHtml) return; /* #ifdef H5 */ const view = window.open('', '_blank'); if (view) { view.document.write(this.newPalletHtml); view.document.close(); view.focus(); view.print() } else this.setError(new Error('请允许打开标签打印窗口'), '打印失败'); /* #endif */ /* #ifndef H5 */ this.setError(new Error('当前设备未配置打印服务'), '打印失败'); /* #endif */ }, + printReturnCarton(label) { if (!label?.printableHtml) return; /* #ifdef H5 */ const view = window.open('', '_blank'); if (view) { view.document.write(label.printableHtml); view.document.close(); view.focus(); view.print() } else this.setError(new Error('请允许打开标签打印窗口'), '打印失败'); /* #endif */ /* #ifndef H5 */ this.setError(new Error('当前设备未配置打印服务'), '打印失败'); /* #endif */ }, acceptTracking(raw) { this.trackingNo = String(raw?.detail?.value ?? raw ?? '').trim(); playSuccessSound() }, - async confirmReceipt() { if (!this.canConfirm || this.busy) return; const willComplete = this.current.expectedItems.every((item) => this.quantity(item.skuCode) >= this.remaining(item)); if (willComplete && !this.photoFileIds.length) { const modal = await new Promise((resolve) => uni.showModal({ title: '缺少退货打托照片', content: '本次签收将完成整单入库,请先拍照并上传后再确认打托。', confirmText: '去拍照', cancelText: '取消', success: resolve })); if (modal?.confirm) { this.error = { message: '请先拍照并上传退货打托照片后再确认' }; } return } const payload = { returnNo: this.current.returnNo, trackingNo: this.trackingNo.trim() || undefined, startedBy: this.startedBy, palletCode: this.palletCode.trim(), items: this.selectedItems.map((item) => ({ skuCode: item.skuCode, receivedQuantity: this.quantity(item.skuCode) })).filter((item) => item.receivedQuantity > 0) }; const key = warehouseIdempotencyKey('returnReceipt', 'confirm', payload); await this.run(async () => { this.result = await confirmReturnReceipt({ ...payload, idempotencyKey: key.value }); releaseWarehouseKey('returnReceipt', key.signature); resetWarehouseDomain('returnReceipt'); this.current = null; this.candidates = null; this.activeTarget = 'lookup'; playSuccessSound() }, '退货入库失败') }, + async confirmReceipt() { if (this.busy) return; if (this.receiptBatch) { await this.run(() => this.resumeReceiptBatch(), '退货入库失败'); return } const totalBoxCount = this.palletBoxCount; if (this.unitType === 'PALLET' && totalBoxCount <= 0) { this.setError(new Error('请先填写各 SKU 的箱数'), '托盘签收校验失败'); return } if (!this.canConfirm || this.busy) return; const willComplete = this.current.expectedItems.every((item) => (this.unitType === 'BOX' ? this.boxRows.reduce((sum, box) => sum + (Number(box.items[item.skuCode]) || 0), 0) : this.quantity(item.skuCode)) >= this.remaining(item)); if (willComplete && !this.photoFileIds.length) { const modal = await new Promise((resolve) => uni.showModal({ title: '缺少退货打托照片', content: '本次签收将完成整单入库,请先拍照并上传后再确认打托。', confirmText: '去拍照', cancelText: '取消', success: resolve })); if (modal?.confirm) this.error = { message: '请先拍照并上传退货打托照片后再确认' }; return } + await this.run(async () => { + const rows = this.unitType === 'BOX' ? this.boxRows : [null] + const entries = rows.map((box, index) => { + const payload = { returnNo: this.current.returnNo, trackingNo: this.trackingNo.trim() || undefined, startedBy: this.startedBy, unitType: this.unitType, unitCode: this.unitType === 'PARCEL' ? this.unitCode.trim() : undefined, palletCode: this.unitType === 'PALLET' || this.unitType === 'BOX' ? this.palletCode.trim() || undefined : undefined, length: this.unitType === 'PALLET' ? Number(this.palletLength) : undefined, width: this.unitType === 'PALLET' ? Number(this.palletWidth) : undefined, height: this.unitType === 'PALLET' ? Number(this.palletHeight) : undefined, weight: this.unitType === 'PALLET' ? Number(this.palletWeight) : undefined, totalBoxCount: this.unitType === 'PALLET' ? totalBoxCount : undefined, items: (this.unitType === 'BOX' ? this.selectedItems.map((item) => ({ skuCode: item.skuCode, receivedQuantity: Number(box.items[item.skuCode]) || 0, boxCount: 1 })) : this.selectedItems.map((item) => ({ skuCode: item.skuCode, receivedQuantity: this.quantity(item.skuCode), boxCount: this.unitType === 'PALLET' ? this.boxCount(item.skuCode) : undefined }))).filter((item) => item.receivedQuantity > 0) } + const key = warehouseIdempotencyKey('returnReceipt', `confirm-${index}`, payload) + return { request: { ...payload, idempotencyKey: key.value }, result: null } + }) + if (this.unitType === 'BOX') { + updateWarehouseWork('returnReceipt', { pendingBatch: JSON.parse(JSON.stringify({ returnNo: this.current.returnNo, entries })) }) + await this.resumeReceiptBatch() + } else { + this.finishReceipt([await confirmReturnReceipt(entries[0].request)]) + } + }, '退货入库失败') + }, + async resumeReceiptBatch() { + const batch = this.receiptBatch + const scopeKey = warehouseWork.scopeKey + if (!batch) return + for (let index = 0; index < batch.entries.length; index++) { + if (warehouseWork.scopeKey !== scopeKey) throw new Error('作业账号或仓库已改变,请重新进入作业') + if (batch.entries[index].result) continue + // Persist the immutable request before every attempt; storage failures must stop submission. + updateWarehouseWork('returnReceipt', { pendingBatch: batch }) + const result = await confirmReturnReceipt(batch.entries[index].request) + if (warehouseWork.scopeKey !== scopeKey) throw new Error('作业账号或仓库已改变,请重新进入作业') + if (!result) throw new Error('服务端未返回签收结果,请重试本批签收') + batch.entries[index].result = result + updateWarehouseWork('returnReceipt', { pendingBatch: batch }) + } + this.finishReceipt(batch.entries.map(entry => entry.result)) + }, + finishReceipt(results) { + const last = results[results.length - 1] + this.result = { ...last, labels: results.filter(item => item.unitType === 'BOX' && item.printableHtml).map(item => ({ cartonMark: item.cartonMark, printableHtml: item.printableHtml })) } + resetWarehouseDomain('returnReceipt') + this.current = null + this.candidates = null + this.activeTarget = 'lookup' + playSuccessSound() + }, manualComplete() { if (!this.current || this.busy) return; uni.showModal({ title: '缺货手动完结', editable: true, placeholderText: '请填写实收少于预期数量的原因', success: async (modal) => { if (!modal.confirm) return; const reason = String(modal.content || '').trim(); if (!reason) { this.setError(new Error('必须填写差异原因'), '手动完结失败'); return } await this.run(async () => { await manuallyCompleteReturnReceipt(this.current.returnNo, reason); this.result = { palletCode: '', receiptNo: '', receiptBatchNo: '', expectedQuantity: this.current.expectedItems.reduce((sum, item) => sum + item.expectedQuantity, 0), receivedQuantity: this.current.expectedItems.reduce((sum, item) => sum + item.receivedQuantity, 0), fullyMatched: false }; this.current = null; this.candidates = null; this.activeTarget = 'lookup'; playSuccessSound() }, '手动完结失败') } }) }, choosePhoto() { if (!this.current || this.busy || this.photoFileIds.length >= 20) return; const no = this.current.returnNo; uni.chooseImage({ count: Math.min(9, 20 - this.photoFileIds.length), sourceType: ['camera', 'album'], success: async ({ tempFilePaths, tempFiles }) => { await this.run(async () => { for (const [index,path] of (tempFilePaths || []).entries()) { if (Number(tempFiles?.[index]?.size || 0) > 10 * 1024 * 1024) throw new Error('照片不能超过 10MB'); const file = await uploadReturnPalletPhoto(no, path); const id = String(file.fileId); this.photoFileIds.push(id); this.photoNames.push(file.fileName || '退货打托照片'); this.photoUrls[id] = path } playSuccessSound() }, '退货打托照片上传失败') } }) }, previewPhoto(fileId) { const urls = this.photoFileIds.map(id => this.photoUrls[id]).filter(Boolean); if (this.photoUrls[fileId]) uni.previewImage({ current: this.photoUrls[fileId], urls }) }, removePhoto(index) { const [id] = this.photoFileIds.splice(index, 1); this.photoNames.splice(index, 1); if (id) delete this.photoUrls[id] }, async reloadCurrent() { if (!this.current?.returnNo || this.busy) return; await this.openOrder(this.current.returnNo, this.startedBy, false) }, - clearCurrent() { resetWarehouseDomain('returnReceipt'); this.current = null; this.candidates = null; this.error = null; this.activeTarget = 'lookup' }, + clearCurrent() { if (this.busy || this.receiptBatch) return; resetWarehouseDomain('returnReceipt'); this.current = null; this.candidates = null; this.error = null; this.activeTarget = 'lookup' }, nextReceipt() { this.result = null; this.clearCurrent() }, async run(work, fallback) { if (this.busy) return; this.busy = true; this.error = null; try { await work() } catch (error) { this.setError(error, fallback) } finally { this.busy = false } }, setError(error, fallback) { this.error = { message: error?.message || fallback, code: error?.code, requestId: error?.requestId, traceId: error?.traceId }; playErrorSound() }, @@ -100,4 +160,7 @@ @import '../warehouse/operations.scss'; .candidate-heading .card-title{margin-bottom:0}.candidate-card{width:100%;text-align:left}.select-link{color:#135ee8;font-size:12px;font-weight:800}.field-label{display:block;margin:13px 0 6px;color:#52667e;font-size:11px;font-weight:700}.remaining-copy{color:#135ee8;font-size:11px}.quantity-row{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:10px;color:#65768b;font-size:11px}.quantity-row .number-input{width:112px;text-align:center}.summary-bar{position:sticky;bottom:0;z-index:4;display:flex;align-items:center;gap:12px;margin-top:12px;padding:11px 13px;border:1px solid #d8e3f1;border-radius:13px;background:#fff;box-shadow:0 5px 20px rgba(22,55,96,.14)}.summary-bar>view{min-width:70px}.summary-label,.summary-value{display:block}.summary-label{color:#728197;font-size:10px}.summary-value{color:#135ee8;font-size:22px;font-weight:800}.submit-button{margin-top:0}.result-card{text-align:left}.result-carton{display:block;margin:9px 0;color:#135ee8;font-size:20px;font-weight:800;word-break:break-all}.link-button{width:auto}.work-card>.link-button{margin-top:8px;padding-left:0}.success-box .primary{margin-top:14px}.scan-hint{display:block;margin-top:9px;color:#65768b;font-size:11px;text-align:center} .photo-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-top:12px}.photo-tile{min-width:0;overflow:hidden;border:1px solid #d7e1ef;border-radius:8px}.photo-thumbnail{display:block;width:100%;height:130px;background:#f1f4f8}.photo-name{display:block;padding:6px 8px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:12px;color:#52647c}.photo-tile .link-button{margin:0 8px 6px} + .unit-tabs{display:flex;gap:8px;margin:10px 0}.unit-tab{flex:1!important;min-width:0!important;height:34px!important;min-height:34px!important;margin:0!important;padding:0 10px!important;border:1px solid #d6e1ef!important;border-radius:8px!important;background:#f7f9fc!important;color:#53677f!important;font-size:13px!important;line-height:34px!important}.unit-tab.active{border-color:#135ee8!important;background:#eaf2ff!important;color:#135ee8!important;font-weight:700} + .dimension-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:7px;margin-top:10px}.dimension-grid label>text{display:block;margin-bottom:3px;color:#65768b;font-size:10px}.dimension-grid .number-input{box-sizing:border-box;width:100%;height:34px;padding:0 8px;border:1px solid #d7e1ef;border-radius:7px;color:#24364b;font-size:13px;text-align:center;background:#fff}.dimension-grid .number-input:focus{border-color:#2167e8;box-shadow:0 0 0 2px rgba(33,103,232,.12)}.volume-box{display:flex;align-items:center;justify-content:center;gap:4px;height:34px;padding:0 6px;border:1px solid #b9d3f4;border-radius:7px;color:#65768b;font-size:10px;background:#f4f8ff}.volume-box strong{color:#135ee8;font-size:13px} + .work-card>.row-head .business-code{font-size:16px;line-height:20px}.work-card>.row-head .meta-line{font-size:10px;line-height:15px}.work-card>.row-head .status-pill{padding:4px 7px;font-size:10px} diff --git a/src/stores/warehouse-work.js b/src/stores/warehouse-work.js index 9c3349c..f980a06 100644 --- a/src/stores/warehouse-work.js +++ b/src/stores/warehouse-work.js @@ -6,8 +6,9 @@ const blankState = () => ({ stocktake: { taskNo: '', scopeType: 'LOCATION', scopeCode: '', reason: '', pendingKeys: {} }, move: { sourcePalletCode: '', targetLocationCode: '', reason: '', pendingKeys: {} }, relabel: { currentNo: '', oldLabel: '', newLabel: '', action: 'RELABEL', reason: '', remark: '', photoFileIds: [], photoNames: [], pendingKeys: {} }, - returnReceipt: { entryMode: 'SKU', lookupCode: '', currentReturnNo: '', startedBy: 'SKU', trackingNo: '', palletCode: '', quantities: {}, pendingKeys: {} }, + returnReceipt: { pendingBatch: null, entryMode: 'SKU', lookupCode: '', currentReturnNo: '', startedBy: 'SKU', trackingNo: '', palletCode: '', quantities: {}, boxCounts: {}, pendingKeys: {} }, returnPacking: { searchCode: '', selectedOrders: [], quantities: {}, length: '', width: '', height: '', weight: '', pendingKeys: {} }, + returnPackingPlan: { pendingKeys: {} }, warehouseTransfer: { currentNo: '', pendingKeys: {} }, query: { inventoryCode: '', locationCode: '', palletCode: '' } }) @@ -40,6 +41,7 @@ export function restoreWarehouseWork(scopeKey) { stocktake: domain(saved.stocktake, fresh.stocktake), move: domain(saved.move, fresh.move), relabel: domain(saved.relabel, fresh.relabel), returnReceipt: domain(saved.returnReceipt, fresh.returnReceipt), returnPacking: domain(saved.returnPacking, fresh.returnPacking), + returnPackingPlan: domain(saved.returnPackingPlan, fresh.returnPackingPlan), warehouseTransfer: domain(saved.warehouseTransfer, fresh.warehouseTransfer), query: { ...fresh.query, ...(saved.query || {}) } }) diff --git a/tests/return-receipt-page.test.mjs b/tests/return-receipt-page.test.mjs index 96eedb7..207db2d 100644 --- a/tests/return-receipt-page.test.mjs +++ b/tests/return-receipt-page.test.mjs @@ -29,9 +29,11 @@ test('PALLET page submits the required palletCode through the actual confirmatio await page.confirmReceipt() assert.equal(calls.length, 1) assert.equal(calls[0].palletCode, 'PLT-TEST') - assert.equal(calls[0].unitCode, undefined) - assert.deepEqual(Object.keys(calls[0]).sort(), ['idempotencyKey', 'items', 'palletCode', 'returnNo', 'startedBy', 'trackingNo'].sort()) - assert.deepEqual(Object.keys(calls[0].items[0]).sort(), ['receivedQuantity', 'skuCode'].sort()) + assert.equal(calls[0].unitCode, undefined) + assert.deepEqual(Object.keys(calls[0]).sort(), ['height', 'idempotencyKey', 'items', 'length', 'palletCode', 'returnNo', 'startedBy', 'totalBoxCount', 'trackingNo', 'unitType', 'weight', 'width'].sort()) + assert.equal(calls[0].unitType, 'PALLET') + assert.equal(calls[0].totalBoxCount, 1) + assert.deepEqual(Object.keys(calls[0].items[0]).sort(), ['boxCount', 'receivedQuantity', 'skuCode'].sort()) assert.equal(calls[0].returnNo, 'RT-TEST') assert.equal(calls[0].trackingNo, 'TRACK-TEST') assert.equal(calls[0].items[0].receivedQuantity, 2)