@@ -6,8 +6,8 @@
< view class = "fulfillment-heading" > < text class = "fulfillment-title" > 出库任务 < / text > < text class = "fulfillment-subtitle" > 一件代发扫描托盘码可整托出库 ; 扫描排车业务号会转入 “ 排车拣货 ” < / text > < / view >
< view v-if = "current" class="detail-toolbar"><view><text class="detail-kicker">当前任务</text><text class="detail-title">出库作业详情</text></view><button :disabled="busy" @tap="closeTask" > ‹ 返回列表 < / button > < / view >
< view class = "fulfillment-card" >
< ScanTargetInput name = "outbound" :model-value = "scanCode" label = "任务号、出库单号、排车单号、托盘码或箱码" placeholder = "扫描任务号 / 出库单号 / 排车单号 / 托盘码 / 箱码" :active = "true" @ update :modelValue = "updateScanCode" @submit ="handleScan" / >
< button class = "secondary-button" : disabled = "!scanCode || loading || busy" @tap ="handleScan(scanCode)" > {{ current ? ' 匹配出库货物 ' : ' 打开出库任务 ' }} < / button >
< ScanTargetInput name = "outbound" :model-value = "scanCode" : label = "discardCurrent ? '废弃订单托盘码' : ' 任务号、出库单号、排车单号、托盘码或箱码' " : placeholder = "discardCurrent ? '扫描废弃订单对应托盘码' : ' 扫描任务号 / 出库单号 / 排车单号 / 托盘码 / 箱码' " :active = "true" @ update :modelValue = "updateScanCode" @submit ="handleScan" / >
< button class = "secondary-button" : disabled = "!scanCode || loading || busy" @tap ="handleScan(scanCode)" > {{ discardCurrent ? ' 打开废弃托盘 ' : current ? ' 匹配出库货物 ' : ' 打开出库任务 ' }} < / button >
< text class = "scan-help" > 任务号直接恢复 ; 进入任务后扫描 DP 托盘码 , 将一次拣取该托全部未拣箱件 。 < / text >
< / view >
@@ -19,7 +19,8 @@
< view v-if = "tasks.length" class="task-list" >
< button v-for = "task in tasks" :key="task.taskNo" class="task-card" @tap="openTask(task.taskNo)"><view class="task-head"><view class="task-code-row"><text class="business-code">{{ task.taskNo || '—' }}</text><text v-if="task.taskNo === recentTaskNo" class="recent-badge">上次作业</text></view><text class="status-badge">{{ statusLabel(task.status) }}</text></view><text class="task-meta">出库单 {{ task.outboundNo || '—' }} · {{ taskType(task) }}</text><text class="task-enter" > 查看并开始出库 › < / text > < / button >
< / view >
< view v-else-if = "!error" class="fulfillment-state"><text class="state-title" > 暂无出库任务 < / text > < text > 可刷新列表 , 或扫描任务号恢复未完成作业 。 < / text > < / view >
< view v-else-if = "!error && !discardTasks.length " class="fulfillment-state"><text class="state-title" > 暂无出库任务 < / text > < text > 可刷新列表 , 或扫描任务号恢复未完成作业 。 < / text > < / view >
< view v-if = "discardTasks.length" class="discard-section"><view class="list-toolbar"><view><text class="list-title">废弃订单</text><text class="list-count">{{ discardOrderCount }} 个订单</text></view></view><view class="task-list"><button v-for="order in discardOrders" :key="order.discardNo" class="task-card" @tap="openDiscardOrder(order.discardNo)"><view class="task-head"><view class="task-code-row"><text class="business-code">{{ order.discardNo || '—' }}</text></view><text class="status-badge">废弃订单</text></view><text class="task-meta">托盘 {{ order.palletCount }} 个 · 待作业 {{ order.pendingPalletCount }} 个</text><text class="task-enter" > 点击进入 , 扫描对应托盘 › < / text > < / button > < / view > < / view >
< / template >
< template v-if = "current" >
@@ -41,6 +42,12 @@
< view class = "fulfillment-card gate-card" > < text class = "card-title" > 完成出库任务 < / text > < text v-if = "remainingPackages > 0" class="gate-warning">尚有 {{ remainingPackages }} 箱未拣,不能完成出库。</text><text v-else-if="current.status === 'COMPLETED'" class="gate-success">出库任务已完成,不可重复提交。</text><text v-else class="gate-note">全部计划货物已扫描核平,服务端将再次校验库存门禁。</text><button v-if="current.status !== 'COMPLETED'" class="primary-button" :disabled="!canComplete || busy" @tap="completeTask" > {{ busyAction = = = ' complete ' ? ' 完成中 … ' : ' 完成出库任务 ' }} < / button > < / view >
< / template >
< template v-else-if = "discardCurrent" >
< view class = "detail-toolbar" > < view > < text class = "detail-kicker" > 废弃订单 < / text > < text class = "detail-title" > { { discardCurrent . discardNo } } < / text > < / view > < button :disabled = "discardBusy" @tap ="closeDiscard" > ‹ 返回列表 < / button > < / view >
< view class = "fulfillment-card" > < ScanTargetInput name = "cargo-discard" :model-value = "discardPalletCode" label = "废弃托盘码" placeholder = "扫描对应托盘码" :active = "true" @ update :modelValue = "updateDiscardPalletCode" @submit ="resolveDiscardPallet" / > < button class = "secondary-button" : disabled = "!discardPalletCode || discardLoading || discardBusy" @tap ="resolveDiscardPallet" > {{ discardLoading ? ' 读取中 … ' : ' 打开托盘废弃作业 ' }} < / button > < text class = "scan-help" > 扫描该废弃订单对应的托盘 , 再填写每个 SKU 的实际废弃数量 。 < / text > < / view >
< view v-if = "discardTask" class="fulfillment-card item-card"><view class="task-head"><text class="business-code">托盘 {{ discardTask.palletNo }}</text><text class="status-badge">{{ discardTask.status === 'COMPLETED' ? '已完成' : '待废弃' }}</text></view><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 }} 箱</text><view class="count-row"><text>实际废弃数量</text><input type="number" :value="discardActuals[sku.itemId]" :disabled="discardTask.status === 'COMPLETED'" :placeholder="`请输入 0-${sku.plannedPackages} 箱`" @input="updateDiscardActual(sku.itemId, $event)" /><text>最多 {{ sku.plannedPackages }} 箱</text></view></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 >
< / scroll-view >
@@ -50,7 +57,7 @@
< script >
import PortalHeader from '@/components/PortalHeader.vue'
import ScanTargetInput from '@/components/ScanTargetInput.vue'
import { completeOutboundTask , getOutboundTask , listOutboundTasks , outboundPickPayload , pickOutboundPallet , pickOutboundTask , resolveOutboundTask } from '@/api/fulfillment.js'
import { completeOutboundTask , confirmCargoDiscard , getOutboundTask , listCargoDiscardTasks , listOutboundTasks , normalizeCargoDiscardTask , outboundPickPayload , pickOutboundPallet , pickOutboundTask , resolveCargoDiscardPallet , resolveOutboundTask } from '@/api/fulfillment.js'
import { fulfillment , fulfillmentIdempotencyKey , releaseFulfillmentKey , restoreFulfillmentDraft , updateFulfillmentDraft } from '@/stores/fulfillment.js'
import { requireSession , session } from '@/stores/session.js'
import { playErrorSound , playSuccessSound } from '@/utils/audio-feedback.js'
@@ -58,7 +65,7 @@
export default {
components : { PortalHeader , ScanTargetInput } ,
data ( ) { return { user : session . currentUser , tasks : [ ] , current : null , selectedItem : null , selectedPosition : null , 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 : { } , discardPalletCode : '' , discardLoading : false , discardBusy : false , selectedItem : null , selectedPosition : null , scanCode : '' , loading : false , busy : false , busyAction : '' , error : null , successMessage : '' , bootstrapped : false } } ,
computed : {
recentTaskNo ( ) { return fulfillment . outbound . currentTaskNo || '' } ,
packages ( ) { return fulfillment . outbound . packages } ,
@@ -67,7 +74,10 @@
remainingPackages ( ) { return Math . max ( 0 , this . plannedPackages - this . pickedPackages ) } ,
selectedRemaining ( ) { return Math . max ( 0 , Math . min ( Number ( this . selectedItem ? . plannedPackages || 0 ) - Number ( this . selectedItem ? . pickedPackages || 0 ) , Number ( this . selectedPosition ? . availablePackages || 0 ) ) ) } ,
canPick ( ) { return this . current ? . status !== 'COMPLETED' && this . selectedItem && this . selectedPosition && Number ( this . packages ) > 0 && Number ( this . packages ) <= this . selectedRemaining } ,
canComplete ( ) { return this . plannedPackages > 0 && this . remainingPackages === 0 && this . current ? . status !== 'COMPLETED' }
canComplete ( ) { return this . plannedPackages > 0 && this . remainingPackages === 0 && this . current ? . status !== 'COMPLETED' } ,
discardOrders ( ) { const groups = new Map ( ) ; for ( const task of this . discardTasks ) { const current = groups . get ( task . discardNo ) || { discardNo : task . discardNo , palletCount : 0 , pendingPalletCount : 0 } ; current . palletCount += 1 ; if ( task . status !== 'COMPLETED' ) current . pendingPalletCount += 1 ; groups . set ( task . discardNo , current ) } return [ ... groups . values ( ) ] } ,
discardOrderCount ( ) { return this . discardOrders . length } ,
canConfirmDiscard ( ) { return Boolean ( this . discardTask && this . discardTask . status !== 'COMPLETED' && this . discardTask . skus . length && this . discardTask . skus . every ( ( sku ) => Number . isInteger ( Number ( this . discardActuals [ sku . itemId ] ) ) && Number ( this . discardActuals [ sku . itemId ] ) >= 0 && Number ( this . discardActuals [ sku . itemId ] ) <= sku . plannedPackages ) ) }
} ,
onShow ( ) { if ( ! requireSession ( ) ) return ; this . user = session . currentUser ; restoreFulfillmentDraft ( ` ${ this . user ? . id || '' } : ${ this . user ? . warehouseId || '' } ` ) ; this . startScanner ( ) ; 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 ( ) } ,
@@ -77,7 +87,7 @@
stopScanner ( ) { if ( this . _stopScanner ) this . _stopScanner ( ) ; this . _stopScanner = null } ,
updateScanCode ( value ) { this . scanCode = value ; updateFulfillmentDraft ( 'outbound' , { scanCode : value } ) } ,
updatePackages ( event ) { const value = Math . trunc ( Number ( event . detail . value || 0 ) ) ; updateFulfillmentDraft ( 'outbound' , { packages : Number . isFinite ( value ) ? Math . max ( 0 , value ) : 0 } ) } ,
async loadTasks ( ) { await this . run ( '加载出库任务' , async ( ) => { this . tasks = await listOutboundTasks ( ) ; this . current = null } , true ) } ,
async loadTasks ( ) { await this . run ( '加载出库任务' , async ( ) => { const [ tasks , discardTasks ] = await Promise . all ( [ listOutboundTasks ( ) , listCargoDiscardTasks ( ) ] ) ; this . tasks = tasks ; this . discardTasks = discardTasks ; this . current = null ; this . discardCurrent = null ; this . discardTask = null } , true ) } ,
async openTask ( taskNo ) { const code = String ( taskNo || '' ) . trim ( ) ; if ( ! code ) return ; await this . run ( '恢复出库任务' , async ( ) => { const pendingScan = fulfillment . outbound . currentTaskNo === code ? fulfillment . outbound . scanCode : '' ; this . current = await getOutboundTask ( code ) ; this . selectedItem = null ; this . selectedPosition = null ; this . scanCode = pendingScan ; updateFulfillmentDraft ( 'outbound' , { currentTaskNo : this . current . taskNo , scanCode : pendingScan , packages : pendingScan ? fulfillment . outbound . packages : 1 } ) ; if ( pendingScan ) this . findAndSelectPosition ( pendingScan , false ) } , true ) } ,
async openScannedTask ( businessNo ) {
await this . run ( '解析出库作业' , async ( ) => {
@@ -95,9 +105,16 @@
} , 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 ) { const code = String ( discardNo || '' ) . trim ( ) ; if ( ! code ) return ; this . discardCurrent = { discardNo : code } ; this . discardTask = null ; this . discardPalletCode = '' ; this . discardActuals = { } ; this . error = null } ,
closeDiscard ( ) { this . discardCurrent = null ; this . discardTask = null ; this . discardPalletCode = '' ; this . discardActuals = { } ; this . loadTasks ( ) } ,
updateDiscardPalletCode ( value ) { this . discardPalletCode = value } ,
async resolveDiscardPallet ( raw ) { const code = String ( raw ? . detail ? . value ? ? raw ? ? this . discardPalletCode ) . trim ( ) ; if ( ! code || ! this . discardCurrent || this . discardBusy ) return ; this . discardPalletCode = code ; this . discardLoading = true ; try { const task = await resolveCargoDiscardPallet ( code ) ; if ( task . discardNo !== this . discardCurrent . discardNo ) throw new Error ( '扫描的托盘不属于当前废弃订单' ) ; this . discardTask = normalizeCargoDiscardTask ( task ) ; this . discardActuals = Object . fromEntries ( this . discardTask . skus . map ( ( sku ) => [ sku . itemId , sku . actualPackages == null ? sku . plannedPackages : sku . actualPackages ] ) ) } catch ( error ) { this . setError ( error , '废弃托盘读取失败' ) } finally { this . discardLoading = 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 . 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 . successMessage = '废弃出库作业已完成,等待仓库确认' ; 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 ) return
console . info ( '[Overseas PDA outbound] scan' , { code , task : this . current ? . taskNo || '' } ) ; this . scanCode = code ; updateFulfillmentDraft ( 'outbound' , { scanCode : code } )
console . info ( '[Overseas PDA outbound] scan' , { code , task : this . current ? . taskNo || '' , discard : this . discardCurrent ? . discardNo || '' } ) ; this . scanCode = code ; updateFulfillmentDraft ( 'outbound' , { scanCode : code } )
if ( this . discardCurrent ) return this . resolveDiscardPallet ( code )
if ( ! this . current ) return this . openScannedTask ( code )
if ( code === this . current . taskNo || code === this . current . outboundNo ) return this . openTask ( this . current . taskNo )
if ( ( this . current . items || [ ] ) . some ( ( item ) => item . palletNo === code ) ) return this . submitPalletPick ( code )