@@ -1,86 +1,86 @@
< template >
< view v-if = "user" class="page-shell portal-page receiving-page" >
< PortalHeader : title = "inboundMode ? $t('operation.title') : operationName" :user = "user" :fallback-url = "`/pages/module/module?key=${moduleKey}`" / >
< 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 = "loading" class="state-card"><uni-load-more status="loading" / > < text > { { $t ( 'operation.restoring' ) } } < / text > < / view >
< view v-if = "loading" class="state-card"><uni-load-more status="loading" / > < text > 正在恢复当前柜作业 … < / text > < / view >
< template v-else-if = "phase === 'candidates'" >
< view class = "scan-card" >
< text class = "section-label" > { { $t ( 'operation.scanContainer' ) } } < / text >
< ScanTargetInput name = "container" v-model = "containerInput" : label="$t('operation.container') " : placeholder="$t('operation.containerPlaceholder') " :active="activeTarget === 'container'" @activate="activeTarget = 'container'" @submit="openContainer" / >
< button class = "primary-button" : disabled = "busy || !containerInput.trim()" @tap ="openContainer(containerInput)" > {{ $ t ( ' operation.queryContainer ' ) }} < / button >
< text class = "section-label" > 扫描柜号 < / text >
< ScanTargetInput name = "container" v-model = "containerInput" label="柜号 " placeholder="扫描或人工输入柜号 " :active="activeTarget === 'container'" @activate="activeTarget = 'container'" @submit="openContainer" / >
< button class = "primary-button" : disabled = "busy || !containerInput.trim()" @tap ="openContainer(containerInput)" > 查询柜号 < / button >
< / view >
< view class = "list-toolbar" > < text > { { $t ( 'operation.pendingContainers' , { count : candidates . length } ) } } < / text > < button :disabled = "busy" @tap ="loadCandidates" > {{ $ t ( ' common.refresh ' ) }} < / button > < / view >
< view v-if = "!candidates.length" class="empty-card"><uni-icons type="info" color="#8090a3" size="34" / > < text > { { $t ( 'operation.emptyContainers' ) } } < / text > < / view >
< view class = "list-toolbar" > < text > { { candidates . length } } 个待卸货集装箱 < / text > < button :disabled = "busy" @tap ="loadCandidates" > 刷新 < / button > < / view >
< view v-if = "!candidates.length" class="empty-card"><uni-icons type="info" color="#8090a3" size="34" / > < text > 暂无待卸货集装箱 < / text > < / view >
< view v-for = "item in candidates" :key="item.containerNo" class="container-card" >
< view class = "card-head" > < view > < text class = "container-no" > { { item . containerNo } } < / text > < text class = "container-meta" > { { item . inboundCodeCount } } { { $t ( 'common.inboundCodes' ) } } · { { item . planCount } } { { $t ( 'common.plans' ) } } < template v-if = "item.plannedPalletCount != null"> · {{ item.plannedPalletCount }} {{ $t('common.pallets') }} </template></text><text v-if="item.destinationCodes?.length" class="container-meta">{{ $t('operation.destination', { value: item.destinationCodes.join(' / ') }) }}</text></view><text class="status-tag" :class="statusTone(item.status)" > {{ statusLabel ( item.status ) }} < / text > < / view >
< view class = "progress-row" > < text > { { $t ( 'operation.receivingProgress' ) } } < / text > < text > < strong > { { item . receivedPackages } } < / strong > / { { item . totalPackages } } { { $t ( 'common.boxes' ) } } < / text > < / view >
< view class = "card-head" > < view > < text class = "container-no" > { { item . containerNo } } < / text > < text class = "container-meta" > { { item . inboundCodeCount } } 个入仓码 · { { item . planCount } } 个计划 < template v-if = "item.plannedPalletCount != null"> · {{ item.plannedPalletCount }} 托 </template></text><text v-if="item.destinationCodes?.length" class="container-meta">目的仓:{{ item.destinationCodes.join(' / ') }}</text></view><text class="status-tag" :class="statusTone(item.status)" > {{ statusLabel ( item.status ) }} < / text > < / view >
< view class = "progress-row" > < text > 收货进度 < / text > < text > < strong > { { item . receivedPackages } } < / strong > / { { item . totalPackages } } 箱 < / text > < / view >
< view class = "progress-track" > < view class = "progress-fill" : style = "{ width: percent(item.receivedPackages, item.totalPackages) + '%' }" > < / view > < / view >
< button class = "primary-button" :disabled = "busy" @tap ="openContainer(item.containerNo)" > {{ item.currentAccount ? $ t ( ' operation.continueCurrent ' ) : $ t ( ' operation.viewDetail ' ) }} < / button >
< button class = "primary-button" :disabled = "busy" @tap ="openContainer(item.containerNo)" > {{ item.currentAccount ? ' 继续当前柜 ' : ' 查看详情 ' }} < / button >
< / view >
< / template >
< template v-else-if = "phase === 'detail'" >
< ReceivingSessionSummary :session = "selected" / >
< view class = "lock-notice" > { { $t ( 'operation.lockedNotice' ) } } < / view >
< view class = "sticky-action" > < button class = "ghost-button" :disabled = "busy" @tap ="cancelDetail" > {{ $ t ( ' operation.backList ' ) }} < / button > < button class = "primary-button" :disabled = "busy" @tap ="prepareContainer" > {{ $ t ( ' operation.selectContainer ' ) }} < / button > < / view >
< view class = "lock-notice" > 选择后账号将锁定当前柜 , 完成前不能切换其他柜号 。 < / view >
< view class = "sticky-action" > < button class = "ghost-button" :disabled = "busy" @tap ="cancelDetail" > 返回列表 < / button > < button class = "primary-button" :disabled = "busy" @tap ="prepareContainer" > 选择此柜并开始拍照 < / button > < / view >
< / template >
< template v-else-if = "phase === 'sign'" >
< ReceivingSessionSummary :session = "current" / >
< view class = "photo-card" >
< view class = "card-head" > < view > < text class = "card-title" > { { $t ( 'operation.unloadPhotos' ) } } < / text > < text class = "container-meta" > { { $t ( 'operation.photoHint' ) } } < / text > < / view > < text class = "status-tag" : class = "photosReady ? 'status-good' : 'status-wait'" > { { uploadedPhotoCount } } / 3 < / text > < / view >
< view class = "card-head" > < view > < text class = "card-title" > 卸货签收照片 < / text > < text class = "container-meta" > 三个固定槽位各拍摄 1 张现场照片 < / text > < / view > < text class = "status-tag" : class = "photosReady ? 'status-good' : 'status-wait'" > { { uploadedPhotoCount } } / 3 < / text > < / view >
< view class = "photo-grid" >
< view v-for = "(step, index) in photoSteps" :key="step.role" class="photo-role" :class="{ complete: photoSlots[index]?.fileId }" >
< view class = "photo-preview" : class = "{ failed: photoSlots[index]?.status === 'error' }" @tap ="previewPhoto(index)" >
< image v-if = "photoSlots[index]?.localPath" class="photo-thumb" :src="photoSlots[index].localPath" mode="aspectFill" / >
< view v-else class = "photo-placeholder" > < uni-icons type = "camera" color = "#8090a3" size = "23" / > < text > { { $t ( 'operation.notAdded' ) } } < / text > < / view >
< view v-else class = "photo-placeholder" > < uni-icons type = "camera" color = "#8090a3" size = "23" / > < text > 未添加 < / text > < / view >
< / view >
< view class = "photo-role-info" >
< view class = "photo-role-head" > < text > { { step . label } } < / text > < text : class = "photoSlots[index]?.fileId ? 'photo-valid' : 'photo-required'" > { { photoSlots [ index ] ? . fileId ? $t ( 'common.uploaded' ) : $t ( 'common.required' ) } } < / text > < / view >
< view class = "photo-role-head" > < text > { { step . label } } < / text > < text : class = "photoSlots[index]?.fileId ? 'photo-valid' : 'photo-required'" > { { photoSlots [ index ] ? . fileId ? '已上传' : '必传' } } < / text > < / view >
< text class = "photo-state" > { { photoStateLabel ( photoSlots [ index ] ) } } < / text >
< text v-if = "photoSlots[index]?.status === 'error'" class="photo-error" > {{ photoSlots [ index ] .errorMessage | | $ t ( ' common.retry ' ) }} < / text >
< text v-if = "photoSlots[index]?.status === 'error'" class="photo-error" > {{ photoSlots [ index ] .errorMessage | | ' 请重试 ' }} < / text >
< / view >
< view class = "photo-actions" > < button :disabled = "busy" @tap ="choosePhoto(index)" > {{ photoSlots [ index ] ? .fileId ? $ t ( ' operation.retake ' ) : $ t ( ' operation.takePhoto ' ) }} < / button > < / view >
< view class = "photo-actions" > < button :disabled = "busy" @tap ="choosePhoto(index)" > {{ photoSlots [ index ] ? .fileId ? ' 重拍 ' : ' 拍照 ' }} < / button > < / view >
< / view >
< / view >
< / view >
< view class = "sticky-action one" > < button class = "primary-button" : disabled = "busy || !photosReady" @tap ="signContainer" > {{ $ t ( ' operation.signAndReceive ' ) }} < / button > < / view >
< view class = "sticky-action one" > < button class = "primary-button" : disabled = "busy || !photosReady" @tap ="signContainer" > 确认签收并开始收货 < / button > < / view >
< / template >
< template v-else-if = "phase === 'receive'" >
< ReceivingSessionSummary :session = "current" / >
< view class = "scan-card" >
< text class = "section-label" > { { $t ( 'operation.scanCartonStep' ) } } < / text >
< ScanTargetInput name = "carton" v-model = "cartonInput" : label="$t('operation.carton') " placeholder="NYEXX2608260001#4-1" :active="activeTarget === 'carton'" @activate="activeTarget = 'carton'" @submit="resolveCarton" / >
< button class = "secondary-button" : disabled = "busy || !cartonInput.trim()" @tap ="resolveCarton(cartonInput)" > {{ $ t ( ' operation.parseCarton ' ) }} < / button >
< text class = "section-label" > 1. 扫描箱码定位入仓码 < / text >
< ScanTargetInput name = "carton" v-model = "cartonInput" label="箱码 " placeholder="NYEXX2608260001#4-1" :active="activeTarget === 'carton'" @activate="activeTarget = 'carton'" @submit="resolveCarton" / >
< button class = "secondary-button" : disabled = "busy || !cartonInput.trim()" @tap ="resolveCarton(cartonInput)" > 解析箱码 < / button >
< template v-if = "activeInbound" >
< view class = "matched-inbound-card" >
< view class = "mode-banner" : class = "activeInbound.destinationType === 'FBA' ? 'fba' : 'overseas'" > < text > { { activeInbound . destinationType === 'FBA' ? $t ( 'operation.fbaMode' ) : $t ( 'operation.overseasMode' ) } } < / text > < text > { { destinationLabel ( activeInbound ) } } < / text > < / view >
< view class = "mode-banner" : class = "activeInbound.destinationType === 'FBA' ? 'fba' : 'overseas'" > < text > { { activeInbound . destinationType === 'FBA' ? 'FBA 按箱数收货' : '海外仓逐箱收货' } } < / text > < text > { { destinationLabel ( activeInbound ) } } < / text > < / view >
< text class = "matched-inbound-code" > { { activeInbound . inboundCode } } < / text >
< view class = "matched-inbound-progress" > < text > { { $t ( 'operation.totalBoxes' , { count : activeInbound . totalPackages } ) } } < / text > < text > { { $t ( 'operation.receivedBoxes' , { count : activeInbound . receivedPackages } ) } } < / text > < text > { { $t ( 'operation.pendingBoxes' , { count : activeInbound . pendingPackages } ) } } < / text > < / view >
< view class = "matched-inbound-progress" > < text > 总计 { { activeInbound . totalPackages } } 箱 < / text > < text > 已收 { { activeInbound . receivedPackages } } 箱 < / text > < text > 待收 { { activeInbound . pendingPackages } } 箱 < / text > < / view >
< / view >
< text class = "section-label" > { { $t ( 'operation.scanPalletStep' ) } } < / text >
< ScanTargetInput name = "pallet" v-model = "palletInput" : label="$t('operation.pallet') " : placeholder="$t('operation.palletPlaceholder') " :active="activeTarget === 'pallet'" @activate="activeTarget = 'pallet'" @submit="resolvePallet" / >
< button class = "secondary-button" : disabled = "busy || !palletInput.trim()" @tap ="resolvePallet(palletInput)" > {{ $ t ( ' operation.verifyPallet ' ) }} < / button >
< view v-if = "resolvedPallet" class="resolved-card" > < text > { { resolvedPallet . palletCode } } < / text > < text > { { resolvedPallet . compatible ? $t ( 'operation.palletMatched' ) : $t ( 'operation.palletMismatch' ) } } < / text > < / view >
< text class = "section-label" > 2. 扫描实物托盘码 < / text >
< ScanTargetInput name = "pallet" v-model = "palletInput" label="托盘码 " placeholder="扫描或人工输入托盘码 " :active="activeTarget === 'pallet'" @activate="activeTarget = 'pallet'" @submit="resolvePallet" / >
< button class = "secondary-button" : disabled = "busy || !palletInput.trim()" @tap ="resolvePallet(palletInput)" > 校验托盘 < / button >
< view v-if = "resolvedPallet" class="resolved-card" > < text > { { resolvedPallet . palletCode } } < / text > < text > { { resolvedPallet . compatible ? '目的仓匹配,可收货' : '托盘不匹配' } } < / text > < / view >
< template v-if = "activeInbound.destinationType === 'FBA'" >
< text class = "section-label" > { { $t ( 'operation.inputCountStep' ) } } < / text >
< input v -model .number = " packageCount " class = "quantity-input" type = "number" inputmode = "numeric" : placeholder = "$t('operation.maxCount', { count: activeInbound.pendingPackages }) " / >
< text class = "section-label" > 3. 输入本次箱数 < / text >
< input v -model .number = " packageCount " class = "quantity-input" type = "number" inputmode = "numeric" : placeholder = "`最多可收 ${ activeInbound.pendingPackages} 箱` " / >
< / template >
< button class = "primary-button receipt-button" : disabled = "!canSubmitReceipt || busy" @tap ="submitReceipt" > {{ activeInbound.destinationType = = = ' FBA ' ? $ t ( ' operation.confirmBulk ' ) : $ t ( ' operation.confirmCarton ' ) }} < / button >
< button class = "primary-button receipt-button" : disabled = "!canSubmitReceipt || busy" @tap ="submitReceipt" > {{ activeInbound.destinationType = = = ' FBA ' ? ' 确认按箱数收货 ' : ' 确认本箱收货 ' }} < / button >
< / template >
< / view >
< view v-if = "lastReceipt" class="success-card" > {{ $ t ( ' operation.receiptSuccess ' , { accepted : lastReceipt.acceptedPackages , inbound : lastReceipt.inboundCode , pending : lastReceipt.pendingPackages } ) }} < / view >
< view class = "sticky-action one" > < button class = "primary-button complete-button" : disabled = "busy || !allInboundsReconciled" @tap ="completeContainer" > {{ allInboundsReconciled ? $ t ( ' operation.completeReady ' ) : $ t ( ' operation.incomplete ' , { count : progress.pending } ) }} < / button > < / view >
< view v-if = "lastReceipt" class="success-card" > 本次已收 {{ lastReceipt.acceptedPackages }} 箱 , {{ lastReceipt.inboundCode }} 剩余 {{ lastReceipt.pendingPackages }} 箱 < / view >
< view class = "sticky-action one" > < button class = "primary-button complete-button" : disabled = "busy || !allInboundsReconciled" @tap ="completeContainer" > {{ allInboundsReconciled ? ' 每个入仓码已核平 , 完成整柜入库 ' : ` 尚有 $ { progress.pending } 箱未收 ` }} < / button > < / view >
< / template >
< template v-else-if = "phase === 'completed'" >
< view class = "completed-card" > < view class = "completed-icon" > ✓ < / view > < text class = "completed-title" > { { $t ( 'operation.completedTitle' ) } } < / text > < text > { { current ? . containerNo } } < / text > < text > { { $t ( 'operation.actualReceipt' , { received : current ? . receivedPackages , total : current ? . totalPackages } ) } } < / text > < button class = "primary-button" @tap ="finishAndReturn" > {{ $ t ( ' operation.returnPending ' ) }} < / button > < / view >
< view class = "completed-card" > < view class = "completed-icon" > ✓ < / view > < text class = "completed-title" > 当前柜已完成入库 < / text > < text > { { current ? . containerNo } } < / text > < text > 实收 { { current ? . receivedPackages } } / { { current ? . totalPackages } } 箱 < / text > < button class = "primary-button" @tap ="finishAndReturn" > 返回待卸货列表 < / button > < / view >
< / template >
< view v-if = "error" class="error-card" >
@@ -90,8 +90,8 @@
< / template >
< template v-else >
< view class = "receiving-heading" > < view > < text class = "receiving-title" > { { operationName } } < / text > < text class = "receiving-subtitle" > { { $t ( 'operation.unavailableSubtitle' ) } } < / text > < / view > < / view >
< view class = "blocked-card" > < uni-icons type = "locked" color = "#8090a3" size = "36" / > < text class = "blocked-title" > { { $t ( 'operation.unavailableTitle' ) } } < / text > < text > { { $t ( 'operation.unavailableDescription' ) } } < / text > < / view >
< view class = "receiving-heading" > < view > < text class = "receiving-title" > { { operationName } } < / text > < text class = "receiving-subtitle" > 当前功能暂未开放 < / text > < / view > < / view >
< view class = "blocked-card" > < uni-icons type = "locked" color = "#8090a3" size = "36" / > < text class = "blocked-title" > 功能暂未开放 < / text > < text > 现有平台 、 客户或内部接口不能由仓库作业员直接调用 。 扫码不会提交 , 也不会产生模拟成功结果 。 < / text > < / view >
< / template >
< / view >
< view v-if = "toast.visible" class="app-toast" :class="toast.type" role="status" > {{ toast.message }} < / view >
@@ -111,11 +111,10 @@
export default {
components : { PortalHeader , ReceivingSessionSummary , ScanTargetInput } ,
data ( ) { return { user : session . currentUser , moduleKey : 'query' , opKey : 'inventoryQuery ' , loading : false , busy : false , bootstrapped : false , detailOpen : false , containerInput : '' , cartonInput : '' , palletInput : '' , packageCount : 1 , error : null , toast : { visible : false , message : '' , type : '' } } } ,
data ( ) { return { user : session . currentUser , moduleKey : 'query' , operationName : '库存查询 ' , loading : false , busy : false , bootstrapped : false , detailOpen : false , containerInput : '' , cartonInput : '' , palletInput : '' , packageCount : 1 , error : null , toast : { visible : false , message : '' , type : '' } } } ,
computed : {
inboundMode ( ) { return this . moduleKey === 'inbound' } ,
operationName ( ) { return this . $t ( ` modules. ${ this . opKey } .name ` ) } ,
photoSteps ( ) { return RECEIVING _PHOTO _ROLES . map ( ( role ) => ( { role , label : this . $t ( ( { LICENSE _PLATE : 'operation.photoLicense' , CONTAINER _SEAL : 'operation.photoSeal' , DOOR _OPEN : 'operation.photoDoor' } ) [ role ] ) } ) ) } ,
photoSteps ( ) { return RECEIVING _PHOTO _ROLES . map ( ( role ) => ( { role , label : ( { LICENSE _PLATE : '车牌照片' , CONTAINER _SEAL : '集装箱封签' , DOOR _OPEN : '开柜现场' } ) [ role ] } ) ) } ,
candidates ( ) { return receiving . candidates } , selected ( ) { return receiving . selected } , current ( ) { return receiving . current } , photoSlots ( ) { return receiving . photoSlots } ,
resolvedCarton ( ) { return receiving . resolvedCarton } , resolvedPallet ( ) { return receiving . resolvedPallet } , lastReceipt ( ) { return receiving . lastReceipt } ,
activeTarget : { get ( ) { return receiving . activeScanTarget } , set ( value ) { receiving . activeScanTarget = value } } ,
@@ -123,38 +122,38 @@
uploadedPhotoCount ( ) { return this . photoSlots . filter ( ( item ) => item ? . fileId ) . length } , photosReady ( ) { return this . uploadedPhotoCount === 3 && ! this . photoSlots . some ( ( item ) => item ? . status === 'uploading' ) } ,
canSubmitReceipt ( ) { if ( ! this . activeInbound || ! this . resolvedPallet ? . compatible ) return false ; if ( this . activeInbound . destinationType === 'FBA' ) { const count = Number ( this . packageCount ) ; return Number . isInteger ( count ) && count > 0 && count <= Number ( this . activeInbound . pendingPackages ) } return Boolean ( this . activeInbound . cartonMark ) } ,
allInboundsReconciled ( ) { const values = this . current ? . inbounds || [ ] ; return values . length > 0 && values . every ( ( item ) => Number ( item . pendingPackages ) === 0 ) } ,
pageTitle ( ) { return this . $t ( ` operationPhase. ${ this . phase } .title ` ) } ,
pageSubtitle ( ) { return this . $t ( ` operationPhase. ${ this . phase } .subtitle ` ) }
pageTitle ( ) { return { candidates : '待卸货集装箱' , detail : '柜号详情' , sign : '拆柜拍照签收' , receive : '当前柜收货' , completed : '入库完成' } [ this . phase ] } ,
pageSubtitle ( ) { return { candidates : '选择集装箱后拍摄 3 张照片完成签收' , detail : '核对柜号与整柜计划箱数' , sign : '完成现场取证后进入收货' , receive : '扫描箱码定位入仓码后开始收货' , completed : '库存与柜级进度已由服务端核平' } [ this . phase ] }
} ,
onLoad ( options ) { this . moduleKey = options . module || 'query' ; this . opKey = options . op || ( this . moduleKey === 'inbound' ? 'receiving' : 'inventoryQuery' ) } ,
onLoad ( options ) { this . moduleKey = options . module || 'query' ; const key = options . op || ( this . moduleKey === 'inbound' ? 'receiving' : 'inventoryQuery' ) ; this . operationName = ( { receiving : '拆柜收货' , stocktake : '库存盘点' , move : '商品移库' , pallet : '托盘管理' , relabelReceive : '换标收货' , relabelExecute : '换标执行' , relabelReview : '换标复核' , inventoryQuery : '库存查询' , locationQuery : '库位查询' , palletQuery : '托盘查询' } ) [ key ] || '库存查询' } ,
onShow ( ) { if ( ! requireSession ( ) ) return ; this . user = session . currentUser ; this . startScanner ( ) ; if ( this . inboundMode && ! this . bootstrapped ) this . bootstrap ( ) } ,
onHide ( ) { this . stopScanner ( ) } , onUnload ( ) { this . stopScanner ( ) } , beforeUnmount ( ) { clearTimeout ( this . _toastTimer ) ; this . stopScanner ( ) } ,
methods : {
statusLabel ( status ) { const key = ( { WAIT _SIGN : 'statusWait ' , SIGNED : 'statusSigned ' , RECEIVING : 'statusReceiving' , COMPLETED : 'statusCompleted' } ) [ status ] ; return key ? this . $t ( ` operation. ${ key } ` ) : status } ,
statusLabel ( status ) { return ( { WAIT _SIGN : '待签收 ' , SIGNED : '已签收 ' , RECEIVING : '收货中' , COMPLETED : '已完成' } ) [ status ] || status } ,
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 || this . $t ( 'modules.inbound.name' ) ) } ,
photoStateLabel ( photo ) { if ( photo ? . status === 'uploading' ) return this . $t ( 'operation.uploading' ) ; if ( photo ? . status === 'error' ) return this . $t ( photo . fileId ? 'operation.retakeFailedKept' : 'operation.uploadFailed' ) ; return this . $t ( photo ? . fileId ? 'operation.previewPhoto' : 'operation.takePhotoHint' ) } ,
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 ( this . $t ( 'operation.scanUnavailable' ) , '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 ( actionKey , work ) { const action = this . $t ( ` operation.actions. ${ actionKey } ` ) ; this . busy = true ; this . error = null ; try { const result = await work ( ) ; this . showToast ( this . $t ( 'operation.actionSuccess' , { action } ) , 'success' ) ; return result } catch ( error ) { this . error = error ; playErrorSound ( ) ; this . showToast ( error . message || this . $t ( 'operation.actionFailed' , { action } ) , 'error' ) ; throw error } finally { this . busy = false } } ,
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 ) } ,
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 . $t ( 'operation.photoLoadFailed' ) ; this . error = error } } ) ) } ,
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 } } ) ) } ,
async applyCurrentReceiving ( value ) { setCurrentReceiving ( value ) ; await this . restorePhotoPreviews ( ) ; this . setScanTargetForCurrent ( value ) } ,
async bootstrap ( ) { this . loading = true ; this . error = null ; this . detailOpen = false ; try { const current = await getCurrentReceiving ( ) ; await this . applyCurrentReceiving ( current ) ; this . bootstrapped = true } catch ( error ) { if ( isNoCurrentReceiving ( error ) ) { resetReceivingDraft ( ) ; await this . loadCandidates ( false ) ; this . bootstrapped = true } else this . error = error } finally { this . loading = false } } ,
async recoverCurrentAfterConflict ( error ) { if ( ! isReceivingConflict ( error ) ) return false ; try { const current = await getCurrentReceiving ( ) ; await this . applyCurrentReceiving ( current ) ; this . detailOpen = false ; this . error = null ; this . showToast ( this . $t ( 'operation.currentConflict' , { container : current . containerNo } ) , 'warning' ) ; return true } catch ( recoveryError ) { this . error = recoveryError ; return false } } ,
async loadCandidates ( feedback = true ) { try { const work = ( ) => listReceivingCandidates ( ) ; receiving . candidates = feedback ? await this . run ( 'refresh ' , work ) : await work ( ) } catch ( _ ) { } } ,
async openContainer ( raw ) { const containerNo = String ( raw ? . detail ? . value ? ? raw ? ? this . containerInput ) . trim ( ) ; if ( ! containerNo || this . busy ) return ; try { receiving . selected = await this . run ( 'query ' , ( ) => getReceivingContainer ( containerNo ) ) ; this . containerInput = containerNo ; this . detailOpen = true ; playSuccessSound ( ) } catch ( error ) { await this . recoverCurrentAfterConflict ( error ) } } ,
async recoverCurrentAfterConflict ( error ) { if ( ! isReceivingConflict ( error ) ) return false ; try { const current = await getCurrentReceiving ( ) ; await this . applyCurrentReceiving ( current ) ; this . detailOpen = false ; this . error = null ; this . showToast ( ` 账号已有未完成柜 ${ current . containerNo } ,已返回当前柜 ` , 'warning' ) ; return true } catch ( recoveryError ) { this . error = recoveryError ; return false } } ,
async loadCandidates ( feedback = true ) { try { const work = ( ) => listReceivingCandidates ( ) ; receiving . candidates = feedback ? await this . run ( '刷新待卸柜 ' , work ) : await work ( ) } catch ( _ ) { } } ,
async openContainer ( raw ) { const containerNo = String ( raw ? . detail ? . value ? ? raw ? ? this . containerInput ) . trim ( ) ; if ( ! containerNo || this . busy ) return ; try { receiving . selected = await this . run ( '查询柜号 ' , ( ) => getReceivingContainer ( containerNo ) ) ; this . containerInput = containerNo ; this . detailOpen = true ; playSuccessSound ( ) } catch ( error ) { await this . recoverCurrentAfterConflict ( error ) } } ,
cancelDetail ( ) { this . detailOpen = false ; receiving . selected = null } ,
async prepareContainer ( ) { if ( ! this . selected ? . containerNo ) return ; const key = idempotencyKey ( 'prepare' , { containerNo : this . selected . containerNo } ) ; try { const value = await this . run ( 'select ' , ( ) => prepareReceivingContainer ( this . selected . containerNo , { idempotencyKey : key . value } ) ) ; releaseIdempotencyKey ( key . signature ) ; setCurrentReceiving ( value ) ; this . setScanTargetForCurrent ( value ) ; this . detailOpen = false } catch ( error ) { await this . recoverCurrentAfterConflict ( error ) } } ,
async prepareContainer ( ) { if ( ! this . selected ? . containerNo ) 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 . $t ( 'operation.actionSuccess' , { action : this . photoSteps [ index ] . label } ) , 'success' ) } catch ( error ) { receiving . photoSlots . splice ( index , 1 , previous ? . fileId ? { ... previous , status : 'error' , errorMessage : error . message || this . $t ( 'operation.retakeFailedKept' ) } : { role : this . photoSteps [ index ] . role , localPath : path , uploaded : false , status : 'error' , errorMessage : error . message || this . $t ( 'operation.uploadFailed' ) } ) ; this . error = error ; playErrorSound ( ) } finally { this . busy = false } } ,
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 } } ,
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 ( 'sign ' , ( ) => signCurrentReceiving ( { photos , idempotencyKey : key . value } ) ) ; releaseIdempotencyKey ( key . signature ) ; setCurrentReceiving ( value ) ; this . clearReceiptContext ( ) } catch ( _ ) { } } ,
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 ( 'carton ' , ( ) => resolveCurrentCarton ( cartonMark ) ) ; receiving . resolvedCarton = value ; receiving . resolvedPallet = null ; this . cartonInput = value . cartonMark ; this . palletInput = '' ; this . packageCount = 1 ; receiving . activeScanTarget = 'pallet' ; playSuccessSound ( ) } catch ( _ ) { } } ,
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 ( 'pallet ' , ( ) => resolveCurrentPallet ( palletCode , this . activeInbound . inboundCode ) ) ; receiving . resolvedPallet = value ; this . palletInput = value . palletCode ; if ( ! value . compatible ) throw Object . assign ( new Error ( this . $t ( 'operation.palletMismatch' ) ) , { 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 ( 'receipt ' , ( ) => 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 ( 'complete ' , ( ) => completeCurrentReceiving ( { idempotencyKey : key . value } ) ) ; releaseIdempotencyKey ( key . signature ) ; setCurrentReceiving ( value ) } catch ( _ ) { } } ,
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 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 ( _ ) { } } ,
async finishAndReturn ( ) { resetReceivingDraft ( ) ; await this . loadCandidates ( false ) } ,
showToast ( message , type ) { clearTimeout ( this . _toastTimer ) ; this . toast = { visible : true , message , type } ; this . _toastTimer = setTimeout ( ( ) => { this . toast . visible = false } , 2600 ) }
}