@@ -11,7 +11,7 @@
< / view >
< view v-if = "loading" class="fulfillment-state" > < text > 正在加载出库任务 … < / text > < / view >
< view v-else-if = "error" class="fulfillment-error" role="alert"><text class="state-title">{{ error.message }}</text><text v-if="error.code">错误码:{{ error.code }}</text><text v-if="error.requestId">requestId: {{ error.requestId }}</text><text v-if="error.traceId">traceId: {{ error.traceId }}</text><text v-if="error.network" class="offline-note">网络连接失败,当前任务、扫码草稿和幂等键均已保留 。</text><button class="secondary-button" :disabled="loading || busy || discardLoading || discardBusy" @tap="retry" > 重试 < / button > < / view >
< view v-else-if = "error" class="fulfillment-error" role="alert"><text class="state-title">{{ error.message }}</text><text v-if="error.code">错误码:{{ error.code }}</text><text v-if="error.requestId">requestId: {{ error.requestId }}</text><text v-if="error.traceId">traceId: {{ error.traceId }}</text><text v-if="error.network" class="offline-note">网络连接失败,请刷新核对任务状态后重试 。</text><button class="secondary-button" :disabled="loading || busy || discardLoading || discardBusy" @tap="retry" > 重试 < / button > < / view >
< template v-if = "!loading && !current && !discardCurrent" >
< view class = "list-toolbar" > < view > < text class = "list-title" > 待处理出库任务 < / text > < text class = "list-count" > { { tasks . length } } 个任务 < / text > < / view > < button :disabled = "loading" @tap ="loadTasks" > 刷新 < / button > < / view >
@@ -44,12 +44,25 @@
< 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" > < 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 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 ="restor eDiscardPhotos">重试读取照片</button></view><button class="primary-button" :disabled="!canConfirmDiscard || discardBusy" @tap="confirmDiscard" > {{ discardBusy ? ' 提交中 … ' : ' 废弃出库 ' }} < / button > < / view >
< view v-if = "successMessage" class="fulfillment-success" role="status" > < text > { { successMessage } } < / text > < / view >
< view v-if = "!discardTask && discardPendingTasks.length" class="fulfillment-card"><text class="card-title">本单待废弃托盘</text><view v-for="task in discardPendingTasks" :key="task.palletNo" class="outbound-line"><view class="task-head"><text class="business-code" > {{ task.palletNo = = = ' 散箱 ' ? ' 散箱 ' : task.palletNo }} < / text > < text > 待废弃 < / text > < / view > < / view > < / view >
< view v-if = "! discardTask && discardPendingTasks.length" class="fulfillment-card"><ScanTargetInput name="cargo-discard" :model-value="discardPalletCode" label="废弃托盘码" placeholder="扫描对应托盘码" :active ="discardTask? .scanMode !== 'CARTON'" @update:modelValue="updateDiscardPalletCode" @submit ="resolv eDiscardPallet" / > < / view >
< view v-if = "discardTask" class="fulfillment-card"><ScanTargetInput v-if="discardNeedsRefresh" name="cargo-discard-retry" :model-value="discardPalletCode" label="重新扫描废弃托盘" placeholder="重新扫描当前托盘码" :active="true" @update:modelValue="updateDiscardPalletCode" @submit="resolveDiscardPallet" /><view v-else class="task-head"><text class="business-code" > {{ discardTask.palletNo }} < / text > < text > { { discardTask . status === 'COMPLETED' ? '已完成' : '待废弃' } } < / text > < / view > < / view >
< view v-if = "discardTask" class="fulfillment-card item-card" >
< view v-for = "sku in discardQuantityLines" :key="sku.itemId" class="outbound-line" >
< text class = "line-code" > { { sku . skuCode || '—' } } < / text >
< view 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 class = "scan-help" > 按现场实点填写 , 未废弃的预留库存将释放 。 < / text >
< / view >
< view v-if = "discardTask.scanMode === 'CARTON'"><view class="discard-carton-heading" role="button" :aria-expanded="discardCartonsExpanded" :aria-label="discardCartonsExpanded ? '收起箱号核对' : '展开箱号核对'" @tap="discardCartonsExpanded = !discardCartonsExpanded"><text class="business-code">箱号核对</text><view class="discard-carton-heading-end"><text class="scan-help">待扫描 {{ discardPendingMarks.length }} 箱</text><view class="discard-carton-chevron" :class="{ expanded: discardCartonsExpanded }" aria-hidden="true" > < / view > < / view > < / view >
< text class = "scan-help" > 已核对 { { discardScannedMarks . length } } / { { discardTask . cartonMarks . length } } 箱 < / text >
< view v-if = "discardCartonsExpanded" >
< view v-for = "mark in discardPendingMarks" :key="mark" class="outbound-line" > < text > { { mark } } < / text > < / view >
< text v-if = "!discardPendingMarks.length" class="scan-help" > 箱号已全部核对 < / text >
< / view > < view v-if = "discardTask.status !== 'COMPLETED'"><text class="business-code">扫描箱号出库</text><ScanTargetInput name="cargo-discard-carton" :model-value="discardCartonCode" label="废弃箱号" placeholder="扫描本次废弃的系统箱号" :active="true" @update:modelValue="discardCartonCode = $event" @submit="scanDiscardCarton" /></view></view><view class="discard-photo-section"><text class="business-code">废弃现场照片(至少 1 张)</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 >
< view v-if = "successMessage && !discardCurrent " class="fulfillment-success" role="status" > < text > { { successMessage } } < / text > < / view >
< / view >
< / scroll-view >
< / view >
@@ -66,8 +79,11 @@
export default {
components : { PortalHeader , ScanTargetInput } ,
data ( ) { return { user : session . currentUser , tasks : [ ] , discardTasks : [ ] , current : null , discardCurrent : null , discardTask : null , discardActuals : { } , discardScannedMarks : [ ] , discardAllCartons : false , discardPhotos : [ ] , discardPhotoUrls : { } , discardPhotoErrors : { } , discardPhotoBusy : false , discardPhotoReady : false , discardPalletCode : '' , discardLoading : false , discardBusy : false , selectedItem : null , selectedPosition : null , selectedScanCode : '' , scanCode : '' , loading : false , busy : false , busyAction : '' , error : null , successMessage : '' , bootstrapped : false } } ,
data ( ) { return { user : session . currentUser , tasks : [ ] , discardTasks : [ ] , current : null , discardCurrent : null , discardTask : null , discardActuals : { } , discardScannedMarks : [ ] , discardCartonsExpanded : true , discardCartonCode : '' , discardPhotos : [ ] , discardPhotoUrls : { } , discardPhotoErrors : { } , discardPhotoBusy : false , discardPhotoReady : false , discardPalletCode : '' , discardLoading : false , discardBusy : false , discardNeedsRefresh : false , selectedItem : null , selectedPosition : null , selectedScanCode : '' , scanCode : '' , loading : false , busy : false , busyAction : '' , error : null , successMessage : '' , bootstrapped : false } } ,
computed : {
// 已扫描箱号保留提交凭据,列表只显示尚未核对的箱子。
discardPendingMarks ( ) { return ( this . discardTask ? . cartonMarks || [ ] ) . filter ( mark => ! this . discardScannedMarks . includes ( mark ) ) } ,
discardQuantityLines ( ) { return ( this . discardTask ? . skus || [ ] ) . filter ( sku => sku . actualQuantityEditable ) } ,
displayItems ( ) {
const items = this . current ? . items || [ ]
if ( this . current ? . outboundType !== 'RETURN_OUTBOUND' ) return items
@@ -101,6 +117,7 @@
canComplete ( ) { return this . plannedPackages > 0 && this . remainingPackages === 0 && this . taskOperable } ,
discardOrders ( ) { const groups = new Map ( ) ; for ( const task of this . discardTasks ) { const current = groups . get ( task . discardNo ) || { discardNo : task . discardNo , outboundNo : task . outboundNo , palletCount : 0 , cartonCount : 0 , pendingPalletCount : 0 } ; if ( task . scanMode === 'CARTON' ) current . cartonCount += ( task . cartonMarks || [ ] ) . length ; else current . palletCount += 1 ; if ( task . status !== 'COMPLETED' ) current . pendingPalletCount += 1 ; groups . set ( task . discardNo , current ) } return [ ... groups . values ( ) ] } ,
discardOrderCount ( ) { return this . discardOrders . length } ,
discardPendingTasks ( ) { return this . discardCurrentTasks . filter ( task => task . status !== 'COMPLETED' ) } ,
discardCurrentTasks ( ) { return this . discardTasks . filter ( task => task . discardNo === this . discardCurrent ? . discardNo ) } ,
discardQuantitySummary ( ) {
const task = this . discardTask
@@ -109,7 +126,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 => this . validDiscardActual ( sku ) ) && ( 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 && 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 ( ) } ,
@@ -137,17 +154,32 @@
} , true )
} ,
closeTask ( ) { this . current = null ; this . selectedItem = null ; this . selectedPosition = null ; this . error = null ; this . successMessage = '' ; updateFulfillmentDraft ( 'outbound' , { currentTaskNo : '' , scanCode : '' , packages : 1 } ) ; this . loadTasks ( ) } ,
openDiscardOrder ( discardNo ) { if ( this . discardPhotoBusy ) return ; this . resetDiscardPhotos ( ) ; const code = String ( discardNo || '' ) . trim ( ) ; if ( ! code || this . discardBusy || this . busy || this . loading ) return ; this . current = null ; this . discardLoading = false ; this . discardCurrent = { discardNo : code , outboundNo : this . discardOrders . find ( order => order . discardNo === code ) ? . outboundNo || '' } ; this . discardTask = null ; this . discardPalletCode = '' ; this . discardActuals = { } ; this . discardScannedMarks = [ ] ; this . discardAllCartons = false ; this . error = null ; const loose = this . discardTasks . find ( task => task . discardNo === code && task . scanMode === 'CARTON' && task . status !== 'COMPLETED' ) ; if ( loose ) { this . discardTask = normalizeCargoDiscardTask ( loose ) ; this . discardActuals = Object . fromEntries ( this . discardTask . skus . map ( sku => [ sku . itemId , sku . plannedPackages ] ) ) ; this . restoreDiscardPhotos ( ) } } ,
openDiscardOrder ( discardNo ) { if ( this . discardPhotoBusy ) return ; this . resetDiscardPhotos ( ) ; const code = String ( discardNo || '' ) . trim ( ) ; if ( ! code || this . discardBusy || this . busy || this . loading ) return ; this . current = null ; this . discardLoading = false ; this . discardCurrent = { discardNo : code , outboundNo : this . discardOrders . find ( order => order . discardNo === code ) ? . outboundNo || '' } ; this . discardTask = null ; this . discardPalletCode = '' ; this . discardActuals = { } ; this . discardScannedMarks = [ ] ; this . discardCartonsExpanded = true ; this . discardCartonCode = '' ; this . discardNeedsRefresh = false ; this . error = null ; this . successMessage = '' ; } ,
closeDiscard ( ) { if ( this . discardBusy || this . discardPhotoBusy ) return ; this . resetDiscardPhotos ( ) ; this . discardLoading = false ; this . discardCurrent = null ; this . discardTask = null ; this . discardPalletCode = '' ; this . discardActuals = { } ; this . loadTasks ( ) } ,
updateDiscardPalletCode ( value ) { this . discardPalletCode = value } ,
// 下方扫描框只核对当前任务的真实箱号,不切换托盘或清空照片。
async scanDiscardCarton ( raw ) {
if ( ! this . discardTask || this . discardTask . status === 'COMPLETED' || this . discardBusy || this . discardLoading || this . discardPhotoBusy ) return
const code = String ( typeof raw === 'string' ? raw : raw ? . detail ? . value ? ? this . discardCartonCode ) . trim ( )
if ( ! code ) return
if ( this . discardNeedsRefresh && ! await this . refreshDiscardTask ( ) ) return
if ( this . discardTask . status === 'COMPLETED' ) return
if ( ! this . discardTask . cartonMarks . includes ( code ) ) { this . setError ( new Error ( '箱号不属于当前废弃任务' ) , '箱号核对失败' ) ; return }
if ( ! this . discardScannedMarks . includes ( code ) ) this . discardScannedMarks = [ ... this . discardScannedMarks , code ]
this . discardCartonCode = '' ; this . error = null ; playSuccessSound ( )
} ,
async resolveDiscardPallet ( raw ) {
const code = String ( typeof raw === 'string' ? raw : raw ? . detail ? . value ? ? this . discardPalletCode ) . trim ( )
if ( ! code || ! this . discardCurrent || this . discardBusy || this . discardLoading || this . discardPhotoBusy ) return
if ( this . discardTask ? . scanMode === 'CARTON' && this . discardTask . cartonMarks . includes ( code ) ) { if ( ! this . discardScannedMarks . includes ( code ) ) this . discardScannedMarks = [ ... this . discardScannedMarks , code ] ; this . error = null ; playSuccessSound ( ) ; return }
if ( this . discardTask ? . scanMode === 'CARTON' && this . discardTask . cartonMarks . includes ( code ) ) { await this . scanDiscardCarton ( code ) ; return }
if ( this . discardTask ? . palletNo === code ) { await this . refreshDiscardTask ( ) ; return }
// 当前托盘可重扫,同一废弃单内也可通过托盘或所选箱唛切换任务。
if ( this . discardTask ? . scanMode === 'CARTON' && ! this . discardTasks . some ( task => task . discardNo === this . discardCurrent . discardNo && ( task . palletNo === code || ( task . cartonMarks || [ ] ) . includes ( code ) ) ) ) { this . setError ( new Error ( '托盘或箱唛不属于当前废弃单所选货物' ) , '箱唛核对失败' ) ; return }
// 有托盘的任务须先扫描本单托盘,箱号仅用于展开后的逐箱核对。
const selectedTasks = this . discardTasks . filter ( task => task . discardNo === this . discardCurrent . discardNo )
if ( selectedTasks . length && ! selectedTasks . some ( task => task . palletNo !== '散箱' ? task . palletNo === code : ( task . cartonMarks || [ ] ) . includes ( code ) ) ) { this . setError ( new Error ( '请先扫描当前废弃单对应的托盘码' ) , '托盘核对失败' ) ; return }
const order = this . discardCurrent
this . resetDiscardPhotos ( ) ; this . discardScannedMarks = [ ] ; this . discardAllCartons = false ; this . discardPalletCode = code ; this . discardLoading = true ; this . discardTask = null ; this . discardActuals = { } ; this . error = null
this . resetDiscardPhotos ( ) ; this . discardScannedMarks = [ ] ; this . discardCartonsExpanded = true ; this . discardCartonCode = '' ; this . discardPalletCode = code ; this . discardLoading = true ; this . discardNeedsRefresh = false ; this . discardTask = null ; this . discardActuals = { } ; this . error = null ; this . successMessage = ''
try {
const task = await resolveCargoDiscardPallet ( code , order . discardNo )
if ( this . discardCurrent !== order ) return
@@ -158,6 +190,29 @@
} catch ( error ) { if ( this . discardCurrent === order ) this . setError ( error , '废弃托盘读取失败' ) }
finally { if ( this . discardCurrent === order ) this . discardLoading = false }
} ,
/** 重扫与重试先读取当前权威任务,只保留仍匹配的箱号和实点数;照片仍归当前托盘。 */
async refreshDiscardTask ( ) {
if ( ! this . discardCurrent || ! this . discardTask || this . discardLoading || this . discardPhotoBusy ) return false
const order = this . discardCurrent ; const previous = this . discardTask
this . discardLoading = true
try {
const latest = await resolveCargoDiscardPallet ( previous . palletNo , order . discardNo )
if ( this . discardCurrent !== order || this . discardTask !== previous ) return false
if ( latest . discardNo !== order . discardNo || latest . palletNo !== previous . palletNo ) throw new Error ( '刷新结果不属于当前废弃托盘' )
this . discardTask = normalizeCargoDiscardTask ( latest )
this . discardScannedMarks = this . discardScannedMarks . filter ( mark => ( this . discardTask . cartonMarks || [ ] ) . includes ( mark ) )
this . discardActuals = Object . fromEntries ( this . discardTask . skus . map ( sku => {
const old = previous . skus . find ( line => line . itemId === sku . itemId )
const unchanged = old && old . plannedPackages === sku . plannedPackages && old . actualQuantityEditable === sku . actualQuantityEditable
return [ sku . itemId , sku . actualPackages ? ? ( unchanged ? this . discardActuals [ sku . itemId ] : sku . actualQuantityEditable ? '' : sku . plannedPackages ) ]
} ) )
this . discardPalletCode = latest . palletNo ; this . discardNeedsRefresh = false ; this . error = null
if ( ! this . discardPhotoReady ) await this . restoreDiscardPhotos ( )
if ( latest . status === 'COMPLETED' ) this . successMessage = '当前托盘已完成废弃,请返回列表核对本单剩余任务'
return true
} catch ( error ) { if ( this . discardCurrent === order ) { this . discardNeedsRefresh = true ; this . setError ( error , '废弃任务刷新失败,请重新扫描' ) } ; return false }
finally { if ( this . discardCurrent === order ) this . discardLoading = false }
} ,
resetDiscardPhotos ( ) { this . discardPhotos = [ ] ; this . discardPhotoUrls = { } ; this . discardPhotoErrors = { } ; this . discardPhotoReady = false } ,
async restoreDiscardPhotos ( ) {
if ( ! this . discardTask || ! this . discardCurrent ) return
@@ -195,7 +250,29 @@
// 未填写、负数、小数或超出预留数均阻止提交,保留输入供仓库修正。
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 . error = null ; this . successMessage = '' ; this . discardBusy = true ; try { const lines = this . discardTask . skus . map ( ( sku ) => ( { itemId : sku . itemId , actualPackages : Number ( this . discardActuals [ sku . itemId ] ) } ) ) ; const photoFileIds = this . discardPhotos . map ( photo => String ( photo . fileId ) ) ; const key = fulfillmentIdempotencyKey ( 'outbound' , 'DISCARD' , { discardNo : this . discardCurrent . discardNo , palletNo : this . discardTask . palletNo , lines , photoFileIds , scannedCartonMarks : this . discardScannedMarks , confirmAllCartons : this . discardAllCartons } ) ; await confirmCargoDiscard ( this . discardCurrent . discardNo , this . discardTask . palletNo , lines , photoFileIds , this . discardScannedMarks , this . discardAllCartons , { idempotencyKey : key . value } ) ; this . resetDiscardPhotos ( ) ; this . discardPalletCode = '' ; this . discardTask = null ; this . discardTasks = await listCargoDiscardTasks ( ) ; this . successMessage = '废弃出库作业及照片已提交,已完成' ; playSuccessSound ( ) } catch ( error ) { this . setError ( error , '废弃出库失败' ) } finally { this . discardBusy = false } } ,
/** 单托完成后刷新同单待办,仍有托盘时明确提示继续扫码。 */
async confirmDiscard ( ) {
if ( this . discardBusy || this . discardLoading || ! this . canConfirmDiscard || ! this . discardTask || ! this . discardCurrent ) return
this . error = null ; this . successMessage = '' ; this . discardBusy = true
const palletNo = this . discardTask . palletNo ; const discardNo = this . discardCurrent . discardNo
try {
if ( this . discardNeedsRefresh ) {
if ( ! await this . refreshDiscardTask ( ) || this . discardTask . status === 'COMPLETED' || ! this . discardPhotoReady || ! this . discardTask . skus . every ( sku => this . validDiscardActual ( sku ) ) || ( this . discardTask . scanMode === 'CARTON' && ! this . discardTask . cartonMarks . every ( mark => this . discardScannedMarks . includes ( mark ) ) ) ) return
}
const lines = this . discardTask . skus . map ( sku => ( { itemId : sku . itemId , actualPackages : Number ( this . discardActuals [ sku . itemId ] ) } ) )
const photoFileIds = this . discardPhotos . map ( photo => String ( photo . fileId ) )
const key = fulfillmentIdempotencyKey ( 'outbound' , 'DISCARD' , { discardNo , palletNo , lines , photoFileIds , scannedCartonMarks : this . discardScannedMarks , confirmAllCartons : false } )
await confirmCargoDiscard ( discardNo , palletNo , lines , photoFileIds , this . discardScannedMarks , false , { idempotencyKey : key . value } )
this . resetDiscardPhotos ( ) ; this . discardPalletCode = '' ; this . discardTask = null ; this . discardScannedMarks = [ ] ; this . discardCartonCode = ''
// 确认已成功时,刷新失败不能被显示为出库失败或宣告整单完成。
try { this . discardTasks = await listCargoDiscardTasks ( ) }
catch ( error ) { this . discardTasks = this . discardTasks . filter ( task => task . discardNo !== discardNo || task . palletNo !== palletNo ) ; this . successMessage = '托盘 ' + palletNo + ' 废弃已完成,其他托盘待办刷新失败,请返回列表重新加载' ; this . setError ( error , '待办刷新失败' ) ; return }
const remaining = this . discardPendingTasks . map ( task => task . palletNo )
this . successMessage = remaining . length ? '托盘 ' + palletNo + ' 废弃已完成,本单还有 ' + remaining . length + ' 个待废弃任务:' + remaining . join ( '、' ) + ',请继续扫描对应托盘' : '本单全部废弃出库作业及照片已提交,已完成'
playSuccessSound ( )
} catch ( error ) { this . discardNeedsRefresh = true ; 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 } )
@@ -226,7 +303,7 @@
const key = fulfillmentIdempotencyKey ( 'outbound' , 'complete' , { taskNo : this . current . taskNo } )
await this . run ( '完成出库任务' , async ( ) => { await completeOutboundTask ( this . current . taskNo , { idempotencyKey : key . value } ) ; this . current = await getOutboundTask ( this . current . taskNo ) ; this . successMessage = '出库任务已完成' ; playSuccessSound ( ) } )
} ,
async retry ( ) { if ( this . discardCurrent ) { if ( this . discardPalletCode ) await this . resolveDiscardPallet ( this . discardPalletCode ) ; return } if ( this . current ? . taskNo ) await this . openTask ( this . current . taskNo ) ; else if ( fulfillment . outbound . currentTaskNo ) await this . openTask ( fulfillment . outbound . currentTaskNo ) ; else if ( this . scanCode ) await this . openScannedTask ( this . scanCode ) ; else await this . loadTasks ( ) } ,
async retry ( ) { if ( this . discardCurrent ) { if ( this . discardTask ) await this . refreshDiscardTask ( ) ; else if ( this . discardPalletCode ) await this . resolveDiscardPallet ( this . discardPalletCode ) ; return } if ( this . current ? . taskNo ) await this . openTask ( this . current . taskNo ) ; else if ( fulfillment . outbound . currentTaskNo ) await this . openTask ( fulfillment . outbound . currentTaskNo ) ; else if ( this . scanCode ) await this . openScannedTask ( this . scanCode ) ; else await this . loadTasks ( ) } ,
async run ( action , work , loading = false ) { if ( this . busy || this . loading ) return ; this . error = null ; this . successMessage = '' ; if ( loading ) this . loading = true ; else { this . busy = true ; this . busyAction = action . includes ( '完成' ) ? 'complete' : 'pick' } try { await work ( ) } catch ( error ) { this . setError ( error , ` ${ action } 失败 ` ) } finally { this . loading = false ; this . busy = false ; this . busyAction = '' } } ,
setError ( error , fallback ) { const forbidden = [ '403' , 'A0403' ] . includes ( String ( error ? . code || '' ) ) ; this . error = { message : forbidden ? '当前账号没有海外仓 PDA 作业权限,请联系管理员授权' : error ? . message || fallback , code : error ? . code , requestId : error ? . requestId , traceId : error ? . traceId , network : Boolean ( error ? . network ) } ; console . error ( '[Overseas PDA outbound] failed' , this . error ) ; playErrorSound ( ) } ,
lineBusinessCode ( position ) { return position ? . cartonMark || position ? . palletNo || position ? . skuCode || position ? . inboundCode || '业务码缺失' } ,
@@ -239,6 +316,12 @@
< style scoped >
@ import '../dispatch/fulfillment.scss' ;
. discard - carton - heading { display : flex ; align - items : center ; justify - content : space - between ; gap : 12 px ; min - height : 40 px ; cursor : pointer ; }
. discard - carton - heading > . business - code { flex - shrink : 0 ; white - space : nowrap ; }
. discard - carton - heading - end { display : flex ; align - items : center ; gap : 12 px ; }
. discard - carton - heading - end . scan - help { margin : 0 ; }
. discard - carton - chevron { flex - shrink : 0 ; width : 8 px ; height : 8 px ; margin - right : 3 px ; border - right : 2 px solid # 718095 ; border - bottom : 2 px solid # 718095 ; transform : rotate ( 45 deg ) ; }
. discard - carton - chevron . expanded { transform : rotate ( 225 deg ) ; }
. discard - photo - section { margin : 14 px 0 ; padding - top : 12 px ; border - top : 1 px solid # d9e3ee } . discard - photo - grid { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; gap : 10 px ; margin : 12 px 0 } . discard - photo - tile image { display : block ; width : 100 % ; height : 120 px ; border - radius : 8 px }
. outbound - line { margin - top : 9 px ; padding : 10 px ; border : 1 px solid # e0e7ef ; border - radius : 8 px ; background : # f8fafc ; }
. outbound - line . selected { border : 2 px solid # 135 ee8 ; background : # edf5ff ; }