From a617386f9d9b0704372b4fae5a2e9bae94ced5bf Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Fri, 28 Aug 2026 12:33:17 +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/pages/operation/operation.vue | 7 ++++--- tests/receiving-photo-ui-contract.test.mjs | 5 ++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/pages/operation/operation.vue b/src/pages/operation/operation.vue index f108bc9..964555c 100644 --- a/src/pages/operation/operation.vue +++ b/src/pages/operation/operation.vue @@ -104,6 +104,7 @@ import { startScanBroadcast } from '@/utils/scan-broadcast.js' import { playErrorSound, playSuccessSound } from '@/utils/audio-feedback.js' import { completeCurrentReceiving, downloadCurrentUnloadPhoto, getCurrentReceiving, getReceivingContainer, listReceivingCandidates, prepareReceivingContainer, receiveCurrentBulk, receiveCurrentCarton, resolveCurrentCarton, resolveCurrentPallet, signCurrentReceiving, uploadCurrentUnloadPhoto } from '@/api/inbound.js' + const MAX_UNLOAD_PHOTO_BYTES = 10 * 1024 * 1024 export default { components: { PortalHeader, ReceivingSessionSummary, ScanTargetInput }, @@ -128,7 +129,7 @@ statusLabel(status) { return ({ WAIT_SIGN: '待签收', SIGNED: '已签收', RECEIVING: '收货中', COMPLETED: '已完成' })[status] || status }, statusTone(status) { return status === 'RECEIVING' ? 'status-current' : status === 'COMPLETED' ? 'status-good' : 'status-wait' }, destinationLabel(item) { return item?.destinationType === 'FBA' ? (item.fbaWarehouseCode || 'FBA') : (item?.destinationWarehouseName || item?.destinationWarehouseCode || '海外仓') }, - photoStateLabel(photo) { if (photo?.status === 'uploading') return '正在上传…'; if (photo?.status === 'error') return photo.fileId ? '重拍失败,已保留原图' : '上传失败,请重试'; return photo?.fileId ? '点击左侧照片可查看大图' : '请拍摄现场照片' }, + photoStateLabel(photo) { if (photo?.status === 'uploading') return '正在上传…'; if (photo?.status === 'error') return photo.errorMessage || (photo.fileId ? '重拍失败,已保留原图' : '上传失败,请重试'); return photo?.fileId ? '点击左侧照片可查看大图' : '请拍摄现场照片' }, startScanner() { this.stopScanner(); this._stopScanner = startScanBroadcast({ onScan: (value) => this.handleHardwareScan(value), onError: (error) => { this.error = error; this.showToast('硬件扫码接收器异常,请检查扫描服务', 'warning') } }) }, stopScanner() { if (typeof this._stopScanner === 'function') this._stopScanner(); this._stopScanner = null }, handleHardwareScan(value) { console.info('[Overseas PDA scan] dispatch', { value, phase: this.phase, target: this.activeTarget, matchedInbound: this.activeInbound?.inboundCode }); if (!this.inboundMode) return this.showToast('该功能缺少海外 PDA 专用接口,扫码未提交', 'warning'); if (this.phase === 'candidates') return this.showToast('请直接点击待卸货集装箱进入详情', 'warning'); if (this.phase !== 'receive') return; if (this.activeTarget === 'pallet' && this.activeInbound) { this.palletInput = value; return this.resolvePallet(value) } this.cartonInput = value; return this.resolveCarton(value) }, async run(action, work) { this.busy = true; this.error = null; try { const result = await work(); this.showToast(`${action}成功`, 'success'); return result } catch (error) { this.error = error; playErrorSound(); this.showToast(error.message || `${action}失败`, 'error'); throw error } finally { this.busy = false } }, @@ -141,8 +142,8 @@ async openContainer(raw) { const containerNo = String(raw?.detail?.value ?? raw ?? '').trim(); if (!containerNo || this.busy) return; try { receiving.selected = await this.run('查询柜号', () => getReceivingContainer(containerNo)); this.detailOpen = true; playSuccessSound() } catch (error) { await this.recoverCurrentAfterConflict(error) } }, cancelDetail() { this.detailOpen = false; receiving.selected = null }, async prepareContainer() { if (!this.selected?.containerNo || this.busy) return; const key = idempotencyKey('prepare', { containerNo: this.selected.containerNo }); try { const value = await this.run('选择当前柜', () => prepareReceivingContainer(this.selected.containerNo, { idempotencyKey: key.value })); releaseIdempotencyKey(key.signature); setCurrentReceiving(value); this.setScanTargetForCurrent(value); this.detailOpen = false } catch (error) { await this.recoverCurrentAfterConflict(error) } }, - choosePhoto(index) { if (this.busy) return; uni.chooseImage({ count: 1, sourceType: ['camera'], success: ({ tempFilePaths }) => { const path = tempFilePaths?.[0]; if (path) this.replacePhoto(index, path) } }) }, - async replacePhoto(index, path) { const previous = receiving.photoSlots[index]; receiving.photoSlots.splice(index, 1, { ...(previous || {}), role: this.photoSteps[index].role, status: 'uploading', errorMessage: '' }); this.busy = true; this.error = null; try { const file = await uploadCurrentUnloadPhoto(path); receiving.photoSlots.splice(index, 1, { ...file, fileId: String(file.fileId), role: this.photoSteps[index].role, localPath: path, uploaded: true, status: 'success' }); this.showToast(`${this.photoSteps[index].label}上传成功`, 'success') } catch (error) { receiving.photoSlots.splice(index, 1, previous?.fileId ? { ...previous, status: 'error', errorMessage: error.message || '重拍失败,已保留原图' } : { role: this.photoSteps[index].role, localPath: path, uploaded: false, status: 'error', errorMessage: error.message || '上传失败,请重试' }); this.error = error; playErrorSound() } finally { this.busy = false } }, + choosePhoto(index) { if (this.busy) return; uni.chooseImage({ count: 1, sourceType: ['camera'], success: ({ tempFilePaths, tempFiles }) => { const path = tempFilePaths?.[0]; const size = Number(tempFiles?.[0]?.size || 0); if (size > MAX_UNLOAD_PHOTO_BYTES) { const message = '照片不能超过10MB'; this.error = Object.assign(new Error(message), { code: 'A0400' }); this.showToast(message, 'error'); playErrorSound(); return } if (path) this.replacePhoto(index, path) } }) }, + async replacePhoto(index, path) { const previous = receiving.photoSlots[index]; receiving.photoSlots.splice(index, 1, { ...(previous || {}), role: this.photoSteps[index].role, status: 'uploading', errorMessage: '' }); this.busy = true; this.error = null; try { const file = await uploadCurrentUnloadPhoto(path); receiving.photoSlots.splice(index, 1, { ...file, fileId: String(file.fileId), role: this.photoSteps[index].role, localPath: path, uploaded: true, status: 'success' }); this.showToast(`${this.photoSteps[index].label}上传成功`, 'success') } catch (error) { receiving.photoSlots.splice(index, 1, previous?.fileId ? { ...previous, status: 'error', errorMessage: error.message || '重拍失败,已保留原图' } : { role: this.photoSteps[index].role, localPath: path, uploaded: false, status: 'error', errorMessage: error.message || '上传失败,请重试' }); this.error = error; this.showToast(error.message || '照片上传失败', 'error'); playErrorSound() } finally { this.busy = false } }, previewPhoto(index) { const path = this.photoSlots[index]?.localPath; if (!path) return; uni.previewImage({ urls: [path], current: path }) }, async signContainer() { if (!this.photosReady) return; const photos = this.photoSlots.map((item, index) => ({ role: this.photoSteps[index].role, fileId: String(item.fileId) })); const key = idempotencyKey('sign', { containerNo: this.current.containerNo, photos }); try { const value = await this.run('签收当前柜', () => signCurrentReceiving({ photos, idempotencyKey: key.value })); releaseIdempotencyKey(key.signature); setCurrentReceiving(value); this.clearReceiptContext() } catch (_) {} }, clearReceiptContext() { receiving.resolvedCarton = null; receiving.resolvedPallet = null; this.cartonInput = ''; this.palletInput = ''; this.packageCount = 1; receiving.activeScanTarget = 'carton' }, diff --git a/tests/receiving-photo-ui-contract.test.mjs b/tests/receiving-photo-ui-contract.test.mjs index ad36e6e..2bf357f 100644 --- a/tests/receiving-photo-ui-contract.test.mjs +++ b/tests/receiving-photo-ui-contract.test.mjs @@ -47,13 +47,16 @@ test('three fixed receiving photo roles and string file IDs are frozen', () => { test('camera replaces only one fixed slot and failed retake preserves its previous success', () => { assert.match(page, /choosePhoto\(index\)[\s\S]*uni\.chooseImage\(\{ count: 1, sourceType: \['camera'\]/) + assert.match(page, /MAX_UNLOAD_PHOTO_BYTES = 10 \* 1024 \* 1024/) + assert.match(page, /tempFiles\?\.\[0\]\?\.size[\s\S]*照片不能超过10MB/) assert.doesNotMatch(page, /sourceType:\s*\['album'\]|从相册选择|换照片/) assert.match(page, /async replacePhoto\(index, path\)[\s\S]*const previous = receiving\.photoSlots\[index\]/) assert.match(page, /await uploadCurrentUnloadPhoto\(path\)[\s\S]*fileId: String\(file\.fileId\)[\s\S]*localPath: path/) assert.match(page, /previous\?\.fileId \? \{ \.\.\.previous, status: 'error'/) assert.match(page, /status: 'uploading'/) assert.match(page, /status: 'error'/) - assert.match(page, /photoStateLabel\(photo\)[\s\S]*已保留原图[\s\S]*上传失败/) + assert.match(page, /photoStateLabel\(photo\)[\s\S]*photo\.errorMessage/) + assert.match(page, /this\.showToast\(error\.message \|\| '照片上传失败', 'error'\)/) assert.match(page, /photoSlots\[index\]\?\.fileId \? '重拍' : '拍照'/) assert.match(page, /previewPhoto\(index\)/) assert.match(page, /uploadedPhotoCount\(\) \{ return this\.photoSlots\.filter\(\(item\) => item\?\.fileId\)\.length \}/)