This commit is contained in:
2026-08-26 17:23:42 +08:00
parent 48d3c32293
commit 5ad9757dd5
5 changed files with 51 additions and 12 deletions
+4
View File
@@ -54,6 +54,7 @@ function rawRequest(method, path, data, token) {
}
export async function overseasRequest(method, path, data, retry = true) {
console.info('[Overseas PDA API] request', { method, path })
const attemptedToken = session.token
let response = await rawRequest(method, path, data, attemptedToken)
if (retry && unauthorized(response)) {
@@ -61,6 +62,9 @@ export async function overseasRequest(method, path, data, retry = true) {
response = await rawRequest(method, path, data, replacementToken)
}
const body = response?.data || {}
console.info('[Overseas PDA API] response', { method, path, statusCode: response?.statusCode, code: body?.code,
message: body?.message, requestId: body?.requestId || response?.header?.['x-request-id'] || response?.header?.['X-Request-Id'],
traceId: body?.traceId || response?.header?.['x-trace-id'] || response?.header?.['X-Trace-Id'] })
if (!successful(response, body)) throw errorFrom(response, '海外作业请求失败')
return body.data
}
+4 -4
View File
@@ -3,7 +3,7 @@
<PortalHeader :title="inboundMode ? '拆柜收货' : operationName" :user="user" :fallback-url="`/pages/module/module?key=${moduleKey}`" />
<view class="portal-content receiving-content">
<template v-if="inboundMode">
<view class="receiving-heading"><view><text class="receiving-title">{{ pageTitle }}</text><text class="receiving-subtitle">{{ pageSubtitle }}</text></view></view>
<view v-if="phase !== 'receive'" class="receiving-heading"><view><text class="receiving-title">{{ pageTitle }}</text><text class="receiving-subtitle">{{ pageSubtitle }}</text></view></view>
<view v-if="loading" class="state-card"><uni-load-more status="loading" /><text>正在恢复当前柜作业…</text></view>
@@ -133,8 +133,8 @@
statusTone(status) { return status === 'RECEIVING' ? 'status-current' : status === 'COMPLETED' ? 'status-good' : 'status-wait' }, percent(received, total) { return total ? Math.min(100, Math.round(Number(received || 0) / Number(total) * 100)) : 0 },
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 ? '点击左侧照片可查看大图' : '请拍摄现场照片' },
startScanner() { this.stopScanner(); this._stopScanner = startScanBroadcast({ onScan: (value) => this.handleHardwareScan(value), onError: (error) => { this.error = error } }) }, stopScanner() { if (typeof this._stopScanner === 'function') this._stopScanner(); this._stopScanner = null },
handleHardwareScan(value) { if (!this.inboundMode) return this.showToast('该功能缺少海外 PDA 专用接口,扫码未提交', 'warning'); if (this.phase === 'candidates') { this.containerInput = value; return this.openContainer(value) } 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) },
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') { this.containerInput = value; return this.openContainer(value) } 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 } },
setScanTargetForCurrent(value) { receiving.activeScanTarget = value?.status === 'WAIT_SIGN' ? 'container' : 'carton' },
async restorePhotoPreviews() { await Promise.all(receiving.photoSlots.filter((photo) => photo?.downloadPath && !photo.localPath).map(async (photo) => { photo.status = 'restoring'; try { photo.localPath = await downloadCurrentUnloadPhoto(photo.downloadPath); photo.status = 'success' } catch (error) { photo.status = 'error'; photo.errorMessage = error.message || '照片加载失败'; this.error = error } })) },
@@ -150,7 +150,7 @@
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' },
async resolveCarton(raw) { const cartonMark = String(raw?.detail?.value ?? raw ?? this.cartonInput).trim(); if (!cartonMark || this.busy) return; try { const value = await this.run('解析箱码', () => resolveCurrentCarton(cartonMark)); receiving.resolvedCarton = value; receiving.resolvedPallet = null; this.cartonInput = value.cartonMark; this.palletInput = ''; this.packageCount = 1; receiving.activeScanTarget = 'pallet'; playSuccessSound() } catch (_) {} },
async resolveCarton(raw) { const cartonMark = String(raw?.detail?.value ?? raw ?? this.cartonInput).trim(); if (!cartonMark) return; if (this.busy) { console.warn('[Overseas PDA carton] ignored while busy', { cartonMark }); return this.showToast('正在处理上一笔扫描,请稍后重试', 'warning') } console.info('[Overseas PDA carton] resolving', { cartonMark, containerNo: this.current?.containerNo }); try { const value = await this.run('解析箱码', () => resolveCurrentCarton(cartonMark)); console.info('[Overseas PDA carton] resolved', { cartonMark: value.cartonMark, inboundCode: value.inboundCode, destinationType: value.destinationType, pendingPackages: value.pendingPackages }); receiving.resolvedCarton = value; receiving.resolvedPallet = null; this.cartonInput = value.cartonMark; this.palletInput = ''; this.packageCount = 1; receiving.activeScanTarget = 'pallet'; playSuccessSound() } catch (error) { console.error('[Overseas PDA carton] failed', { cartonMark, message: error.message, code: error.code, requestId: error.requestId, traceId: error.traceId }) } },
async resolvePallet(raw) { const palletCode = String(raw?.detail?.value ?? raw ?? this.palletInput).trim(); if (!palletCode || !this.activeInbound || this.busy) return; try { const value = await this.run('校验托盘', () => resolveCurrentPallet(palletCode, this.activeInbound.inboundCode)); receiving.resolvedPallet = value; this.palletInput = value.palletCode; if (!value.compatible) throw Object.assign(new Error('托盘不匹配'), { code: 'PALLET_INCOMPATIBLE' }); playSuccessSound() } catch (error) { if (!this.error) this.error = error } },
async submitReceipt() { if (!this.canSubmitReceipt) return; const isFba = this.activeInbound.destinationType === 'FBA'; const payload = isFba ? { inboundCode: this.activeInbound.inboundCode, palletCode: this.resolvedPallet.palletCode, packageCount: Number(this.packageCount) } : { cartonMark: this.activeInbound.cartonMark, palletCode: this.resolvedPallet.palletCode }; const key = idempotencyKey(isFba ? 'bulk-receipt' : 'carton-receipt', payload); try { const result = await this.run('提交实收', () => isFba ? receiveCurrentBulk({ ...payload, idempotencyKey: key.value }) : receiveCurrentCarton({ ...payload, idempotencyKey: key.value })); releaseIdempotencyKey(key.signature); receiving.lastReceipt = result; setCurrentReceiving(await getCurrentReceiving()); this.clearReceiptContext(); playSuccessSound() } catch (_) {} },
async completeContainer() { if (!this.allInboundsReconciled) return; const key = idempotencyKey('complete', { containerNo: this.current.containerNo }); try { const value = await this.run('完成入库', () => completeCurrentReceiving({ idempotencyKey: key.value })); releaseIdempotencyKey(key.signature); setCurrentReceiving(value) } catch (_) {} },
+21 -2
View File
@@ -37,22 +37,41 @@ export function startScanBroadcast({ onScan, onError } = {}) {
onReceive(context, intent) {
try {
plus.android.importClass(intent)
const value = normalizeScanValue(intent.getStringExtra(SCAN_BROADCAST_EXTRA))
if (!value || isDuplicateScan(value)) return
const rawValue = intent.getStringExtra(SCAN_BROADCAST_EXTRA)
const value = normalizeScanValue(rawValue)
console.info('[Overseas PDA broadcast] received', { action: intent.getAction(), extra: SCAN_BROADCAST_EXTRA, rawValue, value })
if (!value) { console.warn('[Overseas PDA broadcast] ignored empty value'); return }
if (isDuplicateScan(value)) { console.info('[Overseas PDA broadcast] ignored duplicate', { value }); return }
onScan(value)
} catch (error) {
console.error('[Overseas PDA broadcast] receive failed', { message: error.message })
if (typeof onError === 'function') onError(error)
}
}
})
const Build = plus.android.importClass('android.os.Build')
const sdkInt = Number(Build.VERSION.SDK_INT)
if (Build.VERSION.SDK_INT >= 33) {
const Context = plus.android.importClass('android.content.Context')
activity.registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED)
} else {
activity.registerReceiver(receiver, filter)
}
let packageName = ''
try { packageName = String(activity.getPackageName()) } catch (_) {}
console.info('[Overseas PDA broadcast] receiver registered', {
action: SCAN_BROADCAST_ACTION,
extra: SCAN_BROADCAST_EXTRA,
packageName,
sdkInt,
exported: sdkInt >= 33
})
} catch (error) {
console.error('[Overseas PDA broadcast] registration failed', {
action: SCAN_BROADCAST_ACTION,
extra: SCAN_BROADCAST_EXTRA,
message: error?.message || String(error)
})
receiver = null
activity = null
if (typeof onError === 'function') onError(error)
+4
View File
@@ -54,6 +54,9 @@ test('login has no committed demo credentials and inbound operation implements t
assert.match(operation, /const payload = isFba \? \{ inboundCode: this\.activeInbound\.inboundCode, palletCode: this\.resolvedPallet\.palletCode, packageCount: Number\(this\.packageCount\) \}/)
assert.doesNotMatch(operation, /skuCode/)
assert.match(operation, /clearReceiptContext\(\)[\s\S]*activeScanTarget = 'carton'/)
assert.match(operation, /\[Overseas PDA scan\] dispatch/)
assert.match(operation, /\[Overseas PDA carton\] resolved/)
assert.match(operation, /\[Overseas PDA carton\] failed[\s\S]*requestId[\s\S]*traceId/)
for (const role of ['LICENSE_PLATE', 'CONTAINER_SEAL', 'DOOR_OPEN']) assert.match(adapter, new RegExp(role))
for (const forbidden of ['customerId', 'planNo', 'allocationId', 'warehouseId', 'locationId']) assert.doesNotMatch(inbound, new RegExp(forbidden))
assert.match(operation, /isReceivingConflict|recoverCurrentAfterConflict/)
@@ -84,6 +87,7 @@ test('receiving phases use a compiled container summary with cabinet and aggrega
assert.match(summary, /padding:10px 12px/)
assert.match(summary, /destination-tags\{[^}]*flex-wrap:wrap/)
assert.doesNotMatch(summary, /destinationSummary|text-overflow:ellipsis/)
assert.match(operation, /v-if="phase !== 'receive'" class="receiving-heading"/)
})
test('Chinese English and Spanish locale scaffolding is installed without exposing unfinished translations', async () => {
+12
View File
@@ -1,5 +1,6 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import {
SCAN_BROADCAST_ACTION,
@@ -16,6 +17,17 @@ test('scanner uses the confirmed PDA broadcast contract', () => {
assert.equal(normalizeScanValue(null), '')
})
test('scanner source keeps receiver diagnostics before page routing', async () => {
const source = await readFile(new URL('../src/utils/scan-broadcast.js', import.meta.url), 'utf8')
assert.match(source, /\[Overseas PDA broadcast\] received/)
assert.match(source, /rawValue[\s\S]*value/)
assert.match(source, /ignored duplicate/)
assert.match(source, /receive failed/)
assert.match(source, /receiver registered/)
assert.match(source, /registration failed/)
assert.match(source, /activity\.getPackageName\(\)/)
})
test('scanner drops only immediate duplicate broadcasts', () => {
resetScanDeduplication()
assert.equal(isDuplicateScan('EL-38201', 1000), false)