@@ -1,24 +1,28 @@
< template >
< view v-if = "user" class="page-shell portal-page transfer-page" >
< PortalHeader title = "仓储转运测量 " :user = "user" fallback -url = " / pages / module / module ? key = inbound " / >
< PortalHeader : title = "entryMode === 'list' ? '仓储转运测量列表' : '转运测量作业' " :user = "user" fallback -url = " / pages / module / module ? key = inbound " / >
< scroll-view scroll -y class = "transfer-scroll" > < view class = "portal-content transfer-content" >
< view class = "transfer-heading" >
< view > < text class = "transfer-title" > 托盘测量 < / text > < text class = "transfer-subtitle" > 按实际托盘分别记录箱规 、 重量和箱数 < / text > < / view >
< view > < text class = "transfer-title" > { { entryMode === 'list' ? '仓储转运测量列表' : '转运测量作业' } } < / text > < text class = "transfer-subtitle" > { { entryMode === 'list' ? '选择待测转运单后进入作业详情' : '按实际托盘分别记录箱规、重量和箱数' } } < / text > < / view >
< button class = "refresh-button" :disabled = "loading" aria -label = " 刷新待测转运单 " @tap ="loadCandidates" > < uni -icons type = "refresh" color = "#135ee8" size = "18" / > < text > { { loading ? '加载中' : '刷新' } } < / text > < / button >
< / view >
< template v-if = "!selected && entryMode === 'list'" >
< view class = "section-heading" > < text > 待测转运单 < / text > < text > { { candidates . length } } 单 < / text > < / view >
< view v-if = "!candidates.length && !loading" class="transfer-empty"><text class="empty-title" > 暂无待测量任务 < / text > < text > 新任务进入后可点击右上角刷新 < / text > < / view >
< view class = "candidate-list" >
< button v-for = "item in candidates" :key="item.transferNo" class="transfer-card" :class="{ selected: selected?.transferNo === item.transferNo }" @tap="select(item)" >
< view class = "transfer-card-head" > < text class = "transfer-no" > { { item . transferNo } } < / text > < text class = "status-pill" > { { statusLabel ( item . status ) } } < / text > < / view >
< view class = "transfer-card-head" > < text class = "transfer-no" > { { item . transferNo } } < / text > < text class = "status-pill" > { { statusLabel ( item . status , item . transferNo ) } } < / text > < / view >
< view class = "task-metrics" > < view > < text > 计划箱数 < / text > < strong > { { item . packageCount || 0 } } < / strong > < / view > < view > < text > { { item . destinationType === 'PRIVATE' ? '目的仓' : 'FBA 目的仓' } } < / text > < strong > { { destinationLabel ( item ) } } < / strong > < / view > < view > < text > 换标 < / text > < strong > { { item . needRelabel ? '需要' : '不需要' } } < / strong > < / view > < / view >
< text class = "transfer-meta" > 入仓码 { { item . inboundCode || '—' } } < / text >
< / button >
< / view >
< / template >
< view v-if = "!selected && entryMode === 'work' && !detailLoading" class="transfer-empty"><text class="empty-title" > 暂无进行中的转运测量作业 < / text > < text > 请先从 “ 仓储转运测量列表 ” 选择待测转运单 。 < / text > < / view >
< view v-if = "detailLoading" class="transfer-empty"><text class="empty-title" > 正在读取转运明细 < / text > < text > 请稍候 , 不要重复选择 < / text > < / view >
< template v-if = "selected && !detailLoading" >
< view class = "operation-detail-heading" > < button class = "back-to-list" @tap ="backToCandidates" > ‹ 返回待测列表 < / button > < view > < text class = "operation-detail-title" > 作业详情 < / text > < text class = "operation-detail-subtitle" > 进入后完成当前转运单的测量 、 打托和拍照 < / text > < / view > < / view >
< view v-if = "isInventoryTransfer" class="transfer-success inventory-transfer-panel"><text class="feedback-title">库存转运 · 无需测量</text><text>该单来源于仓库现有库存,不需要重复测量或上传现场照片。请进入托盘作业,按来源托盘执行整托转运、拆托重打,或与相同目的仓合托。</text><button class="transfer-primary" @tap="openPalletOperations" > 进入托盘作业 < / button > < / view >
< template v-if = "!isInventoryTransfer" >
< view class = "order-summary" >
@@ -81,6 +85,7 @@
import PortalHeader from '@/components/PortalHeader.vue'
import ScanTargetInput from '@/components/ScanTargetInput.vue'
import { session , requireSession } from '@/stores/session.js'
import { restoreWarehouseWork , resetWarehouseDomain , updateWarehouseWork , warehouseWork } from '@/stores/warehouse-work.js'
import { startScanBroadcast } from '@/utils/scan-broadcast.js'
import { bindWarehouseTransferPallet , deleteWarehouseTransferPallet , downloadWarehouseTransferMeasurementPhoto , downloadWarehouseTransferPalletLabel , generateWarehouseTransferPallet , getWarehouseTransferDetail , listWarehouseTransferCandidates , listWarehouseTransferMeasurementPhotos , palletizeWarehouseTransfer , saveWarehouseTransferPalletDraft , uploadWarehouseTransferMeasurementPhoto } from '@/api/warehouse-transfers.js'
@@ -96,7 +101,7 @@ const restoredPallet = (value, index) => ({
export default {
components : { PortalHeader , ScanTargetInput } ,
data ( ) { return { user : session . currentUser , candidates : [ ] , selected : null , pallets : [ newPallet ( ) ] , activePalletKey : '' , photoFileIds : [ ] , photoUrls : { } , photoNames : [ ] , completedPallets : [ ] , completedTransferNo : '' , completedDestination : '' , completedDestinationType : '' , loading : false , detailLoading : false , submitting : false , uploadingPhoto : false , openingLabel : false , error : null , success : false , pendingKey : '' } } ,
data ( ) { return { user : session . currentUser , entryMode : 'work' , pendingTaskNo : '' , candidates : [ ] , selected : null , pallets : [ newPallet ( ) ] , activePalletKey : '' , photoFileIds : [ ] , photoNames : [ ] , photoUrls : { } , completedPallets : [ ] , completedTransferNo : '' , completedDestination : '' , completedDestinationType : '' , loading : false , detailLoading : false , submitting : false , uploadingPhoto : false , openingLabel : false , error : null , success : false , pendingKey : '' } } ,
computed : {
completedDestinationLabel ( ) { return this . destinationLabel ( { fbaWarehouseCode : this . completedDestination , destinationType : this . completedDestinationType } ) } ,
totalCartons ( ) { return this . pallets . reduce ( ( sum , pallet ) => sum + Math . max ( 0 , Number ( pallet . cartonCount ) || 0 ) , 0 ) } ,
@@ -121,7 +126,8 @@ export default {
watch : {
pallets : { deep : true , handler ( ) { if ( ! this . submitting ) this . pendingKey = '' } }
} ,
onShow ( ) { if ( requireSession ( ) ) { this . user = session . currentUser ; this . startScanner ( ) ; this . loadCandidates ( ) } } ,
onShow ( ) { if ( requireSession ( ) ) { this . user = session . currentUser ; restoreWarehouseWork ( ` ${ this . user . id } : ${ this . user . warehouseId } ` ) ; this . startScanner ( ) ; this . loadCandidates ( ) . then ( ( ) => { const activeNo = String ( warehouseWork . warehouseTransfer . currentNo || '' ) ; const taskNo = this . pendingTaskNo || activeNo ; if ( this . entryMode === 'work' && taskNo ) { const active = this . candidates . find ( ( item ) => String ( item . transferNo ) === taskNo ) ; if ( active ) { this . pendingTaskNo = '' ; void this . select ( active ) } } } ) } } ,
onLoad ( options ) { this . entryMode = options ? . mode === 'list' ? 'list' : 'work' ; this . pendingTaskNo = String ( options ? . task || '' ) } ,
onHide ( ) { this . stopScanner ( ) } ,
beforeUnmount ( ) { this . stopScanner ( ) } ,
methods : {
@@ -130,7 +136,7 @@ export default {
startScanner ( ) { this . stopScanner ( ) ; this . _stopScanner = startScanBroadcast ( { onScan : ( value ) => this . handlePalletScan ( value ) , onError : ( error ) => { this . error = error } } ) } ,
stopScanner ( ) { if ( this . _stopScanner ) this . _stopScanner ( ) ; this . _stopScanner = null } ,
handlePalletScan ( value ) { const pallet = this . pallets . find ( ( item ) => item . key === this . activePalletKey && item . generated && ! item . bound ) ; if ( ! pallet ) return ; pallet . bindCode = String ( value || '' ) . trim ( ) ; this . bindPallet ( pallet ) } ,
statusLabel ( status ) { return ( { WAIT _PDA _RECEIVING : '待测量' , APPROVED : '待测量' , PARTIALLY _RECEIVED : '测量中' } ) [ status ] || '待测量' } ,
statusLabel ( status , transferNo = '' ) { if ( String ( status ) === 'WAIT_PDA_RECEIVING' && String ( warehouseWork . warehouseTransfer . currentNo ) === String ( transferNo ) ) return '测量中' ; return ( { WAIT _PDA _RECEIVING : '待测量' , APPROVED : '待测量' , PARTIALLY _RECEIVED : '测量中' } ) [ status ] || '待测量' } ,
rawPalletVolume ( pallet ) { const values = [ pallet . lengthCm , pallet . widthCm , pallet . heightCm ] . map ( Number ) ; return values . every ( ( value ) => value > 0 ) ? values [ 0 ] * values [ 1 ] * values [ 2 ] / 1000000 : 0 } ,
palletVolume ( pallet ) { return this . rawPalletVolume ( pallet ) . toFixed ( 3 ) } ,
palletReady ( pallet ) { return Number . isInteger ( Number ( pallet . cartonCount ) ) && [ pallet . cartonCount , pallet . lengthCm , pallet . widthCm , pallet . heightCm , pallet . grossWeightKg ] . every ( ( value ) => Number ( value ) > 0 ) } ,
@@ -157,14 +163,29 @@ export default {
catch ( error ) { this . error = error ; pallet . busy = false }
} ,
async printDraftPallet ( pallet ) { if ( ! pallet . labelDownloadPath ) { this . error = new Error ( '服务端未返回托盘标签下载地址' ) ; return } ; await this . openLabelPdf ( ( ) => downloadWarehouseTransferPalletLabel ( pallet . labelDownloadPath ) ) } ,
async loadCandidates ( ) { this . loading = true ; this . error = null ; try { this . candidates = await listWarehouseTransferCandidates ( ) } catch ( e ) { this . error = e } finally { this . loading = false } } ,
async loadCandidates ( ) { this . loading = true ; this . error = null ; try { const activeNo = String ( warehouseWork . warehouseTransfer . currentNo || '' ) ; this . candidates = ( await listWarehouseTransferCandidates ( ) ) . map ( ( item ) => String ( item . transferNo ) === activeNo && item . status === 'WAIT_PDA_RECEIVING' ? { ... item , status : 'PARTIALLY_RECEIVED' } : item ) } catch ( e ) { this . error = e } finally { this . loading = false } } ,
async select ( item ) {
if ( this . detailLoading || this . submitting ) return
const requestedNo = String ( item ? . transferNo || '' )
const activeNo = String ( warehouseWork . warehouseTransfer . currentNo || '' )
if ( ! activeNo ) {
const result = await new Promise ( ( resolve ) => uni . showModal ( { title : '开始测量' , content : ` 确认开始测量转运单 ${ requestedNo } 吗?开始后本设备需先完成该单,才能选择下一单。 ` , confirmText : '确认开始' , cancelText : '取消' , success : resolve , fail : ( ) => resolve ( { confirm : false } ) } ) )
if ( ! result . confirm ) return
}
if ( activeNo && activeNo !== requestedNo ) {
const result = await new Promise ( ( resolve ) => uni . showModal ( { title : '存在未完结的测量单' , content : ` 转运单 ${ activeNo } 尚未测量完成,是否继续进入? ` , confirmText : '去继续测量' , cancelText : '取消' , success : resolve , fail : ( ) => resolve ( { confirm : false } ) } ) )
if ( ! result . confirm ) return
item = this . candidates . find ( ( candidate ) => String ( candidate . transferNo ) === activeNo ) || item
}
updateWarehouseWork ( 'warehouseTransfer' , { currentNo : String ( item . transferNo ) } )
this . candidates = this . candidates . map ( ( candidate ) => String ( candidate . transferNo ) === String ( item . transferNo ) ? { ... candidate , status : 'PARTIALLY_RECEIVED' } : candidate )
if ( this . entryMode === 'list' ) { uni . navigateTo ( { url : ` /pages/warehouse-transfer/warehouse-transfer?mode=work&task= ${ encodeURIComponent ( String ( item . transferNo ) ) } ` } ) ; return }
this . detailLoading = true ; this . error = null ; this . success = false ; this . completedPallets = [ ] ; this . completedTransferNo = '' ; this . completedDestination = '' ; this . completedDestinationType = ''
try { this . selected = await getWarehouseTransferDetail ( item . transferNo ) ; this . pendingKey = '' ; this . pallets = this . selected . pallets ? . length ? this . selected . pallets . map ( restoredPallet ) : [ newPallet ( ) ] ; await this . restorePhotos ( item . transferNo , this . selected . measurementPhotos ) }
catch ( e ) { this . selected = null ; this . error = e }
finally { this . detailLoading = false }
} ,
backToCandidates ( ) { if ( this . submitting || this . detailLoading ) return ; this . entryMode = 'list' ; this . selected = null ; this . error = null ; this . pallets = [ newPallet ( ) ] ; this . photoFileIds = [ ] ; this . photoUrls = { } ; this . photoNames = [ ] } ,
addPallet ( ) { this . pendingKey = '' ; this . pallets . push ( newPallet ( ) ) } ,
async removePallet ( index ) { const pallet = this . pallets [ index ] ; if ( ! pallet || pallet . generated || pallet . busy ) return ; pallet . busy = true ; try { if ( pallet . draftId ) await deleteWarehouseTransferPallet ( this . selected . transferNo , pallet . draftId ) ; this . pallets . splice ( index , 1 ) ; this . pendingKey = '' } catch ( error ) { pallet . busy = false ; this . error = error } } ,
async restorePhotos ( transferNo , knownPhotos ) {
@@ -203,7 +224,7 @@ export default {
try {
if ( ! this . pendingKey ) this . pendingKey = ` pda-transfer- ${ this . selected . transferNo } - ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 36 ) . slice ( 2 , 10 ) } `
const payload = { idempotencyKey : this . pendingKey , photoFileIds : [ ... this . photoFileIds ] }
const transferNo = this . selected . transferNo ; const result = await palletizeWarehouseTransfer ( transferNo , payload ) ; this . completedTransferNo = transferNo ; this . completedDestination = result ? . fbaWarehouseCode || this . selected . fbaWarehouseCode || '' ; this . completedDestinationType = result ? . destinationType || this . selected . destinationType || '' ; this . completedPallets = ( result ? . pallets || [ ] ) . filter ( ( pallet ) => pallet ? . palletNo ) ; this . success = true ; this . selected = null ; this . pendingKey = '' ; this . pallets = [ newPallet ( ) ] ; this . photoFileIds = [ ] ; this . photoUrls = { } ; this . photoNames = [ ] ; await this . loadCandidates ( )
const transferNo = this . selected . transferNo ; const result = await palletizeWarehouseTransfer ( transferNo , payload ) ; this . completedTransferNo = transferNo ; this . completedDestination = result ? . fbaWarehouseCode || this . selected . fbaWarehouseCode || '' ; this . completedDestinationType = result ? . destinationType || this . selected . destinationType || '' ; this . completedPallets = ( result ? . pallets || [ ] ) . filter ( ( pallet ) => pallet ? . palletNo ) ; this . success = true ; resetWarehouseDomain ( 'warehouseTransfer' ) ; this . selected = null ; this . pendingKey = '' ; this . pallets = [ newPallet ( ) ] ; this . photoFileIds = [ ] ; this . photoUrls = { } ; this . photoNames = [ ] ; await this . loadCandidates ( )
} catch ( e ) { this . error = e } finally { this . submitting = false }
} ,
async printPallet ( pallet ) { if ( ! pallet ? . labelDownloadPath ) { this . error = new Error ( '服务端未返回托盘标签下载地址,请刷新后重试' ) ; return } ; await this . openLabelPdf ( ( ) => downloadWarehouseTransferPalletLabel ( pallet . labelDownloadPath ) ) } ,
@@ -241,6 +262,7 @@ export default {
. take - photo - button [ disabled ] { color : # 9 aa7b5 ; background : # e7ecf2 ; opacity : 1 }
. print - card { box - sizing : border - box ; margin - bottom : 11 px ; padding : 14 px ; border : 1 px solid # b9d3f4 ; border - radius : 13 px ; background : # f8fbff } . print - row button { width : auto ; margin : 0 ; padding : 0 10 px ; color : # 135 ee8 ; border : 1 px solid # 9 fc0ef ; border - radius : 8 px ; background : # fff } . print - row { display : flex ; align - items : center ; justify - content : space - between ; padding : 10 px 0 ; border - top : 1 px solid # dce8f5 ; color : # 728196 ; font - size : 9 px } . print - code { display : block ; color : # 17314 f ; font - size : 13 px ; font - weight : 850 }
. generate - button { width : 100 % ; margin - top : 12 px ; color : # fff ; background : # 135 ee8 } . generated - card { display : flex ; align - items : center ; justify - content : space - between ; margin - top : 12 px ; padding : 10 px ; border - radius : 9 px ; background : # edf5ff } . generated - card button , . bind - panel > button { width : auto ; margin : 0 ; color : # 135 ee8 ; background : # fff } . bind - panel { margin - top : 10 px } . bound - state { margin - top : 10 px ; padding : 9 px ; border - radius : 8 px ; color : # 08705 f ; font - weight : 800 ; background : # dff5ed }
. operation - detail - heading { display : flex ; align - items : flex - start ; gap : 10 px ; margin : 0 2 px 12 px ; padding : 2 px 0 } . back - to - list { width : auto ; height : 30 px ; margin : 0 ; padding : 0 9 px ; border : 1 px solid # c8d8ec ; border - radius : 8 px ; color : # 135 ee8 ; font - size : 10 px ; line - height : 30 px ; background : # f8fbff } . operation - detail - heading > view { min - width : 0 } . operation - detail - title { display : block ; color : # 243951 ; font - size : 15 px ; font - weight : 850 } . operation - detail - subtitle { display : block ; margin - top : 3 px ; color : # 8794 a4 ; font - size : 9 px ; line - height : 14 px }
. collapse - button { width : 30 px ; height : 28 px ; margin : 0 ; padding : 0 ; color : # 135 ee8 ; font - size : 16 px ; line - height : 28 px ; background : # edf5ff }
. receipt - comparison { display : grid ; grid - template - columns : repeat ( 3 , 1 fr ) ; gap : 7 px ; margin - bottom : 9 px } . receipt - comparison view { padding : 9 px ; border - radius : 9 px ; text - align : center ; background : # fff } . receipt - comparison strong , . receipt - comparison text { display : block } . receipt - comparison strong { color : # 17314 f ; font - size : 15 px } . receipt - comparison text { margin - top : 2 px ; color : # 8390 a1 ; font - size : 8 px } . receipt - comparison . difference { border : 1 px solid # e3a044 ; background : # fff4df } . receipt - comparison . difference strong , . receipt - comparison . difference text { color : # a35b0c ; font - weight : 800 }
< / style >