@@ -45,9 +45,9 @@
< template v-else-if = "discardCurrent" >
< view class = "detail-toolbar" > < view > < text class = "detail-kicker" > 废弃订单 < / text > < text class = "detail-title" > { { discardCurrent . discardNo } } < / text > < text v-if = "discardCurrent.outboundNo" class="task-meta">出库单 {{ discardCurrent.outboundNo }}</text></view><button :disabled="discardBusy" @tap="closeDiscard" > ‹ 返回列表 < / button > < / view >
< view class = "fulfillment-card" > < text class = "card-title" > 本单废弃范围 < / text > < view v-for = "task in discardCurrentTasks" :key="task.palletNo" class="outbound-line"><view class="task-head"><text class="business-code">{{ task.palletNo === '散箱' ? '散箱' : task.palletNo }}</text><text>{{ task.status === 'COMPLETED' ? '已完成' : '待废弃' }}</text></view><text v-for="mark in task.cartonMarks" :key="mark" class="line-code">{{ mark }}</text><text v-if="!task.cartonMarks.length" class="scan-help" > 整托废弃 , 请核对托盘及计划数量 。 < / text > < / view > < / view >
< view class = "fulfillment-card" > < text class = "card-title" > 本单相关托盘及 废弃范围 < / text > < view v-for = "task in discardCurrentTasks" :key="task.palletNo" class="outbound-line"><view class="task-head"><text class="business-code">{{ task.palletNo === '散箱' ? '散箱' : task.palletNo }}</text><text>{{ task.status === 'COMPLETED' ? '已完成' : '待废弃' }}</text></view><text v-for="mark in task.cartonMarks" :key="mark" class="line-code">{{ mark }}</text><text v-if="!task.cartonMarks.length" class="scan-help" > 缺少箱唛与托盘对应关系 , 请核对相关托盘 , 以仓库实际废弃箱数为准 。 < / text > < / view > < / view >
< view class = "fulfillment-card" > < ScanTargetInput name = "cargo-discard" :model-value = "discardPalletCode" : label = "discardTask?.scanMode === 'CARTON' ? '废弃箱唛' : '废弃托盘码'" : placeholder = "discardTask?.scanMode === 'CARTON' ? '扫描本单所选箱唛' : '扫描对应托盘码'" :active = "true" @ update :modelValue = "updateDiscardPalletCode" @submit ="resolveDiscardPallet" / > < / view >
< view v-if = "discardTask" class="fulfillment-card item-card"><view class="task-head"><text class="business-code">{{ discardTask.scanMode === 'CARTON' ? (discardTask.palletNo === '散箱' ? '散箱废弃' : '按箱废弃 · ' + discardTask.palletNo) : '托盘 ' + discardTask.palletNo }}</text><text class="status-badge">{{ discardTask.status === 'COMPLETED' ? '已完成' : '待废弃' }}</text></view><text class="task-meta">本次废弃 {{ discardQuantitySummary }}</text><view v-for="sku in discardTask.skus" :key="sku.itemId" class="outbound-line"><text class="line-code">{{ sku.skuCode || '—' }}</text><text class="line-meta">{{ sku.productName || '—' }} · {{ sku.plannedPackages }} {{ sku.quantityUnit || '箱' }}</text></view><view v-if="discardTask.scanMode === 'CARTON'"><text class="scan-help">已核对 {{ discardScannedMarks.length }}/{{ discardTask.cartonMarks.length }} 箱</text><view v-for="mark in discardTask.cartonMarks" :key="mark" class="outbound-line"><text>{{ mark }}</text><text>{{ discardAllCartons ? '全部废弃' : discardScannedMarks.includes(mark) ? '已扫描' : '待扫描' }}</text></view><button v-if="discardTask.status !== 'COMPLETED'" class="secondary-button" :disabled="discardBusy || discardLoading || discardPhotoBusy" @tap="discardAllCartons = true">一键废弃本单所选箱子({{ discardTask.cartonMarks.length }} 箱)</button><text v-if="discardAllCartons" class="scan-help">已确认本单所选箱子全部废弃,拍照后点击废弃出库完成。</text></view><view class="discard-photo-section"><text class="business-code">废弃现场照片(至少 1 张)</text><text class="scan-help">照片关联当前废弃作业,完成出库后可在废弃单详情查看。</text><view class="discard-photo-grid"><view v-for="photo in discardPhotos" :key="photo.fileId" class="discard-photo-tile"><image v-if="discardPhotoUrls[photo.fileId]" :src="discardPhotoUrls[photo.fileId]" mode="aspectFill" @tap="previewDiscardPhoto(photo.fileId)" /><button v-else class="secondary-button" @tap="loadDiscardPhoto(photo.fileId)">{{ discardPhotoErrors[photo.fileId] ? '加载失败,点击重试' : '加载照片' }}</button></view></view><button v-if="discardTask.status !== 'COMPLETED'" class="secondary-button" :disabled="discardLoading || discardBusy || discardPhotoBusy || !discardPhotoReady || discardPhotos.length >= 20" @tap="takeDiscardPhoto">{{ discardPhotoBusy ? '照片上传中…' : '拍摄废弃现场照片' }}( {{ discardPhotos.length }}/20) </button><button v-if="!discardPhotoReady" class="secondary-button" :disabled="discardLoading || discardPhotoBusy" @tap="restoreDiscardPhotos">重试读取照片</button></view><button class="primary-button" :disabled="!canConfirmDiscard || discardBusy" @tap="confirmDiscard" > {{ discardBusy ? ' 提交中 … ' : ' 废弃出库 ' }} < / button > < / view >
< view v-if = "discardTask" class="fulfillment-card item-card"><view class="task-head"><text class="business-code">{{ discardTask.scanMode === 'CARTON' ? (discardTask.palletNo === '散箱' ? '散箱废弃' : '按箱废弃 · ' + discardTask.palletNo) : '托盘 ' + discardTask.palletNo }}</text><text class="status-badge">{{ discardTask.status === 'COMPLETED' ? '已完成' : '待废弃' }}</text></view><text class="task-meta">本次废弃 {{ discardQuantitySummary }}</text><view v-for="sku in discardTask.skus" :key="sku.itemId" class="outbound-line"><text class="line-code">{{ sku.skuCode || '—' }}</text><text class="line-meta">{{ sku.productName || '—' }} · 申请 {{ sku.plannedPackages }} {{ sku.quantityUnit || '箱' }}</text><view v-if="sku.actualQuantityEditable" class="quantity-row"><text>实际废弃箱数</text><input type="number" placeholder="请输入实际箱数" :value="discardActuals[sku.itemId] ?? ''" :disabled="discardTask.status === 'COMPLETED' || discardBusy || discardLoading || discardPhotoBusy" @input="updateDiscardActual(sku.itemId, $event)" /><text>箱(0– {{ sku.plannedPackages }}) </text></view><text v-else-if="sku.actualPackages != null" class="line-meta">实际废弃 {{ sku.actualPackages }} {{ sku.quantityUnit || '箱' }}</text><text v-if="sku.actualQuantityEditable" class="scan-help">按现场实点填写,未废弃的预留库存将释放。</text>< /view><view v-if="discardTask.scanMode === 'CARTON'"><text class="scan-help">已核对 {{ discardScannedMarks.length }}/{{ discardTask.cartonMarks.length }} 箱</text><view v-for="mark in discardTask.cartonMarks" :key="mark" class="outbound-line"><text>{{ mark }}</text><text>{{ discardAllCartons ? '全部废弃' : discardScannedMarks.includes(mark) ? '已扫描' : '待扫描' }}</text></view><button v-if="discardTask.status !== 'COMPLETED'" class="secondary-button" :disabled="discardBusy || discardLoading || discardPhotoBusy" @tap="discardAllCartons = true">一键废弃本单所选箱子({{ discardTask.cartonMarks.length }} 箱)</button><text v-if="discardAllCartons" class="scan-help">已确认本单所选箱子全部废弃,拍照后点击废弃出库完成。</text></view><view class="discard-photo-section"><text class="business-code">废弃现场照片(至少 1 张)</text><text class="scan-help">照片关联当前废弃作业,完成出库后可在废弃单详情查看。</text><view class="discard-photo-grid"><view v-for="photo in discardPhotos" :key="photo.fileId" class="discard-photo-tile"><image v-if="discardPhotoUrls[photo.fileId]" :src="discardPhotoUrls[photo.fileId]" mode="aspectFill" @tap="previewDiscardPhoto(photo.fileId)" /><button v-else class="secondary-button" @tap="loadDiscardPhoto(photo.fileId)">{{ discardPhotoErrors[photo.fileId] ? '加载失败,点击重试' : '加载照片' }}</button></view></view><button v-if="discardTask.status !== 'COMPLETED'" class="secondary-button" :disabled="discardLoading || discardBusy || discardPhotoBusy || !discardPhotoReady || discardPhotos.length >= 20" @tap="takeDiscardPhoto">{{ discardPhotoBusy ? '照片上传中…' : '拍摄废弃现场照片' }}( {{ discardPhotos.length }}/20) </button><button v-if="!discardPhotoReady" class="secondary-button" :disabled="discardLoading || discardPhotoBusy" @tap="restoreDiscardPhotos">重试读取照片</button></view><button class="primary-button" :disabled="!canConfirmDiscard || discardBusy" @tap="confirmDiscard" > {{ discardBusy ? ' 提交中 … ' : ' 废弃出库 ' }} < / button > < / view >
< / template >
< view v-if = "successMessage" class="fulfillment-success" role="status" > < text > { { successMessage } } < / text > < / view >
@@ -110,7 +110,7 @@
const boxes = new Set ( task . cartonMarks || [ ] ) . size || task . skus . filter ( sku => sku . quantityUnit !== '件' ) . reduce ( ( total , sku ) => total + sku . plannedPackages , 0 )
return ` ${ boxes } 箱 ${ pieces ? ` · ${ pieces } 件 ` : '' } `
} ,
canConfirmDiscard ( ) { const task = this . discardTask ; return Boolean ( this . discardPhotoReady && this . discardPhotos . length > 0 && ! this . discardPhotoBusy && ! this . discardLoading && ! this . discardBusy && task && task . status !== 'COMPLETED' && task . skus . length && task . skus . every ( sku => Number ( this . discardActuals [ sku . itemId ] ) === sku . plannedPackages ) && ( task . scanMode !== 'CARTON' || ( task . cartonMarks ? . length && ( this . discardAllCartons || task . cartonMarks . every ( mark => this . discardScannedMarks . includes ( mark ) ) ) ) ) ) }
canConfirmDiscard ( ) { const task = this . discardTask ; return Boolean ( this . discardPhotoReady && this . discardPhotos . length > 0 && ! this . discardPhotoBusy && ! this . discardLoading && ! this . discardBusy && task && task . status !== 'COMPLETED' && task . skus . length && task . skus . every ( sku => this . validDiscardActual ( sku ) ) && ( task . scanMode !== 'CARTON' || ( task . cartonMarks ? . length && ( this . discardAllCartons || task . cartonMarks . every ( mark => this . discardScannedMarks . includes ( mark ) ) ) ) ) ) }
} ,
onShow ( ) { if ( ! requireSession ( ) ) return ; this . user = session . currentUser ; restoreFulfillmentDraft ( ` ${ this . user ? . id || '' } : ${ this . user ? . warehouseId || '' } ` ) ; this . startScanner ( ) ; if ( this . discardPhotoBusy || this . discardBusy ) return ; if ( this . discardCurrent ) { if ( this . discardTask ) this . restoreDiscardPhotos ( ) ; return } if ( ! this . bootstrapped ) { this . bootstrapped = true ; this . scanCode = '' ; this . loadTasks ( ) ; return } if ( this . current ? . taskNo ) this . openTask ( this . current . taskNo ) ; else this . loadTasks ( ) } ,
onHide ( ) { this . stopScanner ( ) } ,
@@ -155,7 +155,7 @@
if ( task . discardNo !== order . discardNo ) throw new Error ( '扫描的托盘不属于当前废弃订单' )
this . discardTask = normalizeCargoDiscardTask ( task )
if ( this . discardTask . scanMode === 'CARTON' && this . discardTask . cartonMarks . includes ( code ) ) this . discardScannedMarks = [ code ]
this . discardActuals = Object . fromEntries ( this . discardTask . skus . map ( ( sku ) => [ sku . itemId , sku . actualPackages == null ? sku . plannedPackages : sku . actualPackages ] ) ) ; await this . restoreDiscardPhotos ( )
this . discardActuals = Object . fromEntries ( this . discardTask . skus . map ( ( sku ) => [ sku . itemId , sku . actualPackages == null ? ( sku . actualQuantityEditable ? '' : sku . plannedPackages ) : sku . actualPackages ] ) ) ; await this . restoreDiscardPhotos ( )
} catch ( error ) { if ( this . discardCurrent === order ) this . setError ( error , '废弃托盘读取失败' ) }
finally { if ( this . discardCurrent === order ) this . discardLoading = false }
} ,
@@ -193,8 +193,10 @@
finally { this . discardPhotoBusy = false }
} , fail : ( ) => { this . discardPhotoBusy = false } } )
} ,
updateDiscardActual ( itemId , event ) { const value = Math . max ( 0 , Math . trunc ( Number ( event . detail . value || 0 ) ) ) ; this . discardActuals = { ... this . discardActuals , [ itemId ] : value } } ,
async confirmDiscard ( ) { if ( this . discardBusy || this . discardLoading || ! this . canConfirmDiscard || ! this . discardTask || ! this . discardCurrent ) return ; this . discardBusy = true ; try { const lines = this . discardTask . skus . map ( ( sku ) => ( { itemId : sku . itemId , actualPackages : sku . plannedPackages } ) ) ; await confirmCargoDiscard ( this . discardCurrent . discardNo , this . discardTask . palletNo , lines , this . discardPhotos . map ( photo => String ( photo . fileId ) ) , this . discardScannedMarks , this . discardAllCartons ) ; this . successMessage = '废弃出库作业及照片已提交,已完成' ; this . resetDiscardPhotos ( ) ; this . discardPalletCode = '' ; this . discardTask = null ; this . discardTasks = await listCargoDiscardTasks ( ) ; playSuccessSound ( ) } catch ( error ) { this . setError ( error , '废弃出库失败' ) } finally { this . discardBusy = false } } ,
// 未填写、负数、小数或超出预留数均阻止提交,保留输入供仓库修正。
validDiscardActual ( sku ) { const raw = this . discardActuals [ sku . itemId ] ; if ( raw == null || String ( raw ) . trim ( ) === '' ) return false ; const actual = Number ( raw ) ; return Number . isSafeInteger ( actual ) && actual >= 0 && ( sku . actualQuantityEditable === true ? actual <= sku . plannedPackages : actual === sku . plannedPackages ) } ,
updateDiscardActual ( itemId , event ) { this . discardActuals = { ... this . discardActuals , [ itemId ] : event . detail . value } } ,
async confirmDiscard ( ) { if ( this . discardBusy || this . discardLoading || ! this . canConfirmDiscard || ! this . discardTask || ! this . discardCurrent ) return ; this . discardBusy = true ; try { const lines = this . discardTask . skus . map ( ( sku ) => ( { itemId : sku . itemId , actualPackages : Number ( this . discardActuals [ sku . itemId ] ) } ) ) ; await confirmCargoDiscard ( this . discardCurrent . discardNo , this . discardTask . palletNo , lines , this . discardPhotos . map ( photo => String ( photo . fileId ) ) , this . discardScannedMarks , this . discardAllCartons ) ; this . successMessage = '废弃出库作业及照片已提交,已完成' ; this . resetDiscardPhotos ( ) ; this . discardPalletCode = '' ; this . discardTask = null ; this . discardTasks = await listCargoDiscardTasks ( ) ; playSuccessSound ( ) } catch ( error ) { this . setError ( error , '废弃出库失败' ) } finally { this . discardBusy = false } } ,
handleScan ( raw ) {
const code = String ( raw ? . detail ? . value ? ? raw ? ? this . scanCode ) . trim ( ) ; if ( ! code || this . busy || this . loading || this . discardBusy || this . discardLoading ) return
console . info ( '[Overseas PDA outbound] scan' , { code , task : this . current ? . taskNo || '' , discard : this . discardCurrent ? . discardNo || '' } ) ; this . scanCode = code ; updateFulfillmentDraft ( 'outbound' , { scanCode : code } )