@@ -29,9 +29,26 @@
< / template >
< / view >
< view class = "work-card" > < view class = "row-head" > < view > < text class = "card-title" > { { unitType === 'PARCEL' ? '逐包裹实收数量' : ( unitType === 'PALLET' ? '本托实收数量' : '逐箱实收数量' ) } } < / text > < / view > < button v-if = "unitType === 'PALLET'" class="link-button" @tap="fillRemaining">全部填入</button></view><view v-if="unitType === 'PALLET'" class="quantity-row"><text>本托箱数合计</text><strong class="auto-count">{{ palletBoxCount }}</strong></view><template v-if="unitType !== 'PALLET'"><view class="quantity-row"><text>{{ unitType === 'PARCEL' ? '本次包裹数' : '本次箱数' }}</text><input class="number-input" type="number" :value="boxRows.length" :placeholder="unitType === 'PARCEL' ? '包裹数' : '箱数'" @input="resizeBoxRows($event.detail.value)" /></view><view v-for="(box,index) in boxRows" :key="index" class="item-card"><text class="business-code">第 {{ index + 1 }} {{ unitType === 'PARCEL' ? '包裹' : '箱' }}</text><view v-for="item in selectedItems" :key="item.skuCode" class="quantity-row"><text>{{ item.skuCode }} · 数量</text><text class="remaining-copy">剩余 {{ remaining(item) }}</text><input class="number-input" type="number" :value="box.items[item.skuCode] || ''" placeholder="0" @input="setBoxQuantity(index, item.skuCode, $event.detail.value)" /></view></view></template><view v-else class="item-list receipt-sku-list"><view v-for="item in selectedItems" :key="item.skuCode" class="item-card"><view class="row-head"><view><text class="business-code">{{ item.skuCode }}</text><text class="meta-line">{{ item.productNameZh || '商品名称未填写' }}</text></view><text class="remaining-copy">剩余 {{ remaining(item) }}</text></view><view class="sku-quantity-fields"><view class="quantity-row"><text>数量</text><input class="number-input" type="number" :value="quantity(item.skuCode)" placeholder="0" @input="setQuantity(item, $event.detail.value)" /></view><view v-if="unitType === 'PALLET'" class="quantity-row"><text>SKU 箱数</text><input class="number-input" type="number" :value="boxCount(item.skuCode)" placeholder="0" @input="setBoxCount(item, $event.detail.value)" / > < / view > < / view > < / view > < / view > < / view >
< view class = "summary-bar" > < view > < text class = "summary-label" > { { unitType === 'PARCEL' ? '本次合计' : '本托合计' } } < / text > < text class = "summary-value" > { { receiptQuantity } } < / text > < / view > < button class = "primary submit-button" : disabled = "!canConfirm || busy" @tap ="confirmReceipt" > {{ busy ? ' 提交中 … ' : unitType = = = ' PARCEL ' ? ' 确认包裹签收 ' : ' 确认签收并入托 ' }} < / button > < / view >
< view class = "work-card" > < view class = "row-head" > < view > < text class = "card-title" > 退货打托照片 < / text > < text class = "hint" > 签收过程中可随时拍照 < / text > < / view > < text class = "status-pill" > { { photoFileIds . length } } / 20 张 < / text > < / view > < button class = "secondary" : disabled = "busy || photoFileIds.length >= 20" @tap ="choosePhoto" > 拍照或从相册选择 < / button > < view v-if = "photoFileIds.length" class="photo-grid"><view v-for="(fileId,index) in photoFileIds" :key="fileId" class="photo-tile"><image class="photo-thumbnail" :src="photoUrls[fileId]" mode="aspectFill" @tap="previewPhoto(fileId)" /><text class="photo-name">{{ photoNames[index] || '退货打托照片' }} </text><butt on class="link-button" :disabled="busy" @tap="removePhoto(index)" > 移除 < / button > < / view > < / view > < / view >
< view class = "work-card" >
< view class = "row-head receipt-quantity-heading" > < text class = "card-title" > { { unitType === 'PARCEL' ? '逐包裹实收数量' : unitType === 'PALLET' ? '本托实收数量' : '逐箱实收数量' } } < / text > < button v-if = "unitType === 'PALLET'" class="link-button" @tap="fillRemaining">全部填入</button><view v-else class="unit-count-row"><text>{{ unitType === 'PARCEL' ? '本次包裹数' : '本次箱数' }}</text><input class="number-input" type="number" placeholder="数量" :value="boxRows.length" :disabled="busy" @input="resizeBoxRows($event.detail.value)" / > < / view > < / view >
< view v-if = "unitType === 'PALLET'" class="quantity-row"><text>本托箱数合计 </text><str ong class="auto-count" > {{ palletBoxCount }} < / strong > < / view >
< template v-if = "unitType !== 'PALLET'" >
< view v-for = "(box,index) in boxRows" :key="index" class="item-card physical-unit-card" >
< view class = "unit-card-head" > < text class = "business-code" > 第 { { index + 1 } } { { unitType === 'PARCEL' ? '包裹' : '箱' } } < / text > < button v-if = "box.confirmed" class="link-button" :disabled="busy" @tap="editBoxRow(index)">已确认 · 展开修改</button><text v-else class="unit-card-tag" > 实测信息 < / text > < / view >
< template v-if = "box.confirmed"><text class="meta-line">{{ box.length }} × {{ box.width }} × {{ box.height }} cm · {{ box.weight }} kg · {{ box.originalPackaging ? '原包装' : '新包装' }}</text><text v-for="item in selectedItems.filter(item => Number(box.items[item.skuCode]) > 0)" :key="item.skuCode" class="meta-line" > {{ item.skuCode }} · {{ box.items [ item.skuCode ] }} 件 < / text > < / template >
< template v-else >
< view class = "dimension-grid unit-dimension-grid" > < label > < text > 长 ( cm ) < / text > < input class = "number-input" type = "digit" v-model = "box.length" placeholder="长" :disabled="busy" /></label><label><text>宽(cm) </text><input class="number-input" type="digit" v-model="box.width" placeholder="宽" :disabled="busy" /></label><label><text>高(cm) </text><input class="number-input" type="digit" v-model="box.height" placeholder="高" :disabled="busy" /></label><label><text>重量(kg) </text><input class="number-input" type="digit" v-model="box.weight" placeholder="重量" :disabled="busy" / > < / label > < / view >
< view class = "unit-volume" > < text > 体积 < / text > < strong > { { rowVolume ( box ) } } < / strong > < text > cm³ < / text > < / view >
< view class = "packaging-choice" > < text > 是否使用原 { { unitType === 'PARCEL' ? '包装' : '箱' } } < / text > < button : class = "['choice-button', {selected: box.originalPackaging === true}]" :disabled = "busy" @tap ="setOriginalPackaging(index, true)" > 是 < / button > < button : class = "['choice-button', {selected: box.originalPackaging === false}]" :disabled = "busy" @tap ="setOriginalPackaging(index, false)" > 否 < / button > < / view >
< view v-for = "item in selectedItems" :key="item.skuCode" class="quantity-row"><text>{{ item.skuCode }} · 数量</text><input class="number-input unit-quantity-input" type="number" :value="box.items[item.skuCode] || ''" :placeholder="'剩余 ' + boxRemaining(index, item)" :disabled="busy" @input="setBoxQuantity(index, item.skuCode, $event.detail.value)" / > < / view >
< button v-if = "boxRowValid(index)" class="primary" :disabled="busy" @tap="confirmBoxRow(index)" > 确认第 {{ index + 1 }} {{ unitType = = = ' PARCEL ' ? ' 包裹 ' : ' 箱 ' }} < / button >
< / template >
< / view >
< / template >
< view v-if = "unitType === 'PALLET'" class="item-list receipt-sku-list"><view v-for="item in selectedItems" :key="item.skuCode" class="item-card"><view class="row-head"><view><text class="business-code">{{ item.skuCode }}</text><text class="meta-line">{{ item.productNameZh || '商品名称未填写' }}</text></view><text class="remaining-copy">剩余 {{ remaining(item) }}</text></view><view class="sku-quantity-fields"><view class="quantity-row"><text>数量</text><input class="number-input" type="number" :value="quantity(item.skuCode)" placeholder="0" @input="setQuantity(item, $event.detail.value)" /></view><view v-if="unitType === 'PALLET'" class="quantity-row"><text>SKU 箱数</text><input class="number-input" type="number" :value="boxCount(item.skuCode)" placeholder="0" @input="setBoxCount(item, $event.detail.value)" / > < / view > < / view > < / view > < / view > < / view >
< view v-if = "unitType === 'PALLET'" class="summary-bar"><view><text class="summary-label">{{ unitType === 'PARCEL' ? '本次合计' : '本托合计' }}</text><text class="summary-value">{{ receiptQuantity }}</text></view><button class="primary submit-button" :disabled="!canConfirm || busy" @tap="confirmReceipt" > {{ busy ? ' 提交中 … ' : unitType = = = ' PARCEL ' ? ' 确认包裹签收 ' : ' 确认签收并入托 ' }} < / button > < / view >
< view class = "work-card" > < view class = "row-head" > < view > < text class = "card-title" > 退货打托照片 < / text > < text class = "hint" > { { unitType !== 'PALLET' ? '支持多张照片,上传后点击确认签收入库' : '签收过程中可随时拍照' } } < / text > < / view > < text class = "status-pill" > { { photoFileIds . length } } / 20 张 < / text > < / view > < button class = "secondary" : disabled = "busy || photoFileIds.length >= 20" @tap ="choosePhoto()" > 拍照或从相册选择 < / button > < view v-if = "photoFileIds.length" class="photo-grid"><view v-for="(fileId,index) in photoFileIds" :key="fileId" class="photo-tile"><image class="photo-thumbnail" :src="photoUrls[fileId]" mode="aspectFill" @tap="previewPhoto(fileId)" /><text class="photo-name">{{ photoNames[index] || '退货打托照片' }}</text><button class="link-button" :disabled="busy" @tap="removePhoto(index)" > 移除 < / button > < / view > < / view > < / view >
< view v-if = "unitType !== 'PALLET'" class="summary-bar"><view><text class="summary-label">本次合计</text><text class="summary-value">{{ receiptQuantity }}</text></view><button class="primary submit-button" :disabled="busy || !canConfirm || !allBoxRowsConfirmed || !photoFileIds.length" @tap="confirmReceipt" > {{ busy ? ' 提交中 … ' : ' 确认签收入库 ' }} < / button > < / view >
< button v-if = "current.status === 'PARTIALLY_RECEIVED'" class="secondary manual-complete" :disabled="busy" @tap="manualComplete" > 缺货手动完结 < / button >
< view v-if = "current.completedPallets?.length" class="work-card"><text class="card-title">本单已完成打托</text><view class="item-list"><view v-for="pallet in current.completedPallets" :key="pallet.palletCode" class="item-card"><text class="business-code">{{ pallet.palletCode }}</text><text class="meta-line">{{ pallet.skuCode }} · {{ pallet.quantity }} 件</text><text class="meta-line" > 签收批次 : {{ pallet.receiptBatchNo }} < / text > < / view > < / view > < / view >
< / template >
@@ -54,7 +71,7 @@
export default {
components : { PortalHeader , ScanTargetInput } ,
data ( ) { return { user : session . currentUser , current : null , candidates : null , result : null , selectedSku : '' , palletCheck : null , newPalletHtml : '' , unitType : 'PALLET' , unitCode : '' , boxRows : [ { items : { } } ] , palletLength : '' , palletWidth : '' , palletHeight : '' , palletWeight : '' , unitOptions : [ { value : 'PALLET' , label : '托盘' } , { value : 'BOX' , label : '箱子' } , { value : 'PARCEL' , label : '包裹' } ] , photoFileIds : [ ] , photoNames : [ ] , photoUrls : { } , activeTarget : 'lookup' , busy : false , error : null } } ,
data ( ) { return { user : session . currentUser , current : null , candidates : null , result : null , selectedSku : '' , palletCheck : null , newPalletHtml : '' , unitType : 'PALLET' , unitCode : '' , boxRows : [ { items : { } , length : '' , width : '' , height : '' , weight : '' , originalPackaging : true , confirmed : false } ] , palletLength : '' , palletWidth : '' , palletHeight : '' , palletWeight : '' , unitOptions : [ { value : 'PALLET' , label : '托盘' } , { value : 'BOX' , label : '箱子' } , { value : 'PARCEL' , label : '包裹' } ] , photoFileIds : [ ] , photoNames : [ ] , photoUrls : { } , activeTarget : 'lookup' , busy : false , error : null } } ,
computed : {
receiptBatch ( ) { return warehouseWork . returnReceipt . pendingBatch } ,
entryMode : { get ( ) { return warehouseWork . returnReceipt . entryMode } , set ( value ) { updateWarehouseWork ( 'returnReceipt' , { entryMode : value } ) } } ,
@@ -63,10 +80,11 @@
trackingNo : { get ( ) { return warehouseWork . returnReceipt . trackingNo } , set ( value ) { updateWarehouseWork ( 'returnReceipt' , { trackingNo : value } ) } } ,
palletCode : { get ( ) { return warehouseWork . returnReceipt . palletCode } , set ( value ) { updateWarehouseWork ( 'returnReceipt' , { palletCode : value } ) } } ,
selectedItems ( ) { return this . current ? . expectedItems || [ ] } ,
allBoxRowsConfirmed ( ) { return this . unitType !== 'PALLET' && this . boxRows . length > 0 && this . boxRows . every ( ( box , index ) => box . confirmed && this . boxRowValid ( index ) ) } ,
palletBoxCount ( ) { return this . selectedItems . reduce ( ( sum , item ) => sum + this . boxCount ( item . skuCode ) , 0 ) } ,
activeSkuCode ( ) { return this . selectedItems . find ( item => item . skuCode === this . selectedSku ) ? . skuCode || this . selectedItems [ 0 ] ? . skuCode || '' } ,
receiptQuantity ( ) { return this . unitType !== 'PALLET' ? this . boxRows . reduce ( ( sum , box ) => sum + Object . values ( box . items ) . reduce ( ( n , quantity ) => n + ( Number ( quantity ) || 0 ) , 0 ) , 0 ) : Object . values ( warehouseWork . returnReceipt . quantities || { } ) . reduce ( ( total , value ) => total + ( Number ( value ) || 0 ) , 0 ) } ,
canConfirm ( ) { if ( ! this . current || ! this . selectedItems . length || this . receiptQuantity <= 0 ) return false ; if ( this . unitType !== 'PALLET' ) return this . boxRows . length > 0 && this . boxRows . every ( box => Object . values ( box . items ) . some ( quantity => Number ( quantity ) > 0 ) ) && this . selectedItems . every ( item => this . boxRows . reduce ( ( sum , box ) => sum + ( Number ( box . items [ item . skuCode ] ) || 0 ) , 0 ) <= this . remaining ( item ) ) ; return ( this . unitType === 'PARCEL' || ( this . palletCode . trim ( ) && this . palletCheck && Number ( this . palletLength ) > 0 && Number ( this . palletWidth ) > 0 && Number ( this . palletHeight ) > 0 && Number ( this . palletWeight ) > 0 && ( this . unitType !== 'PALLET' || this . palletBoxCount > 0 ) ) ) && this . selectedItems . every ( ( item ) => this . quantity ( item . skuCode ) <= this . remaining ( item ) && ( this . unitType !== 'PALLET' || this . quantity ( item . skuCode ) === 0 || this . boxCount ( item . skuCode ) > 0 ) ) ; } ,
canConfirm ( ) { if ( ! this . current || ! this . selectedItems . length || this . receiptQuantity <= 0 ) return false ; if ( this . unitType !== 'PALLET' ) return this . boxRows . length > 0 && this . boxRows . every ( box => Object . values ( box . items ) . some ( quantity => Number ( quantity ) > 0 ) && ( ! ( 'length' in box ) || ( Number ( box . length ) > 0 && Number ( box . width ) > 0 && Number ( box . height ) > 0 && Number ( box . weight ) > 0 && typeof box . originalPackaging === 'boolean' ) ) ) && this . selectedItems . every ( item => this . boxRows . reduce ( ( sum , box ) => sum + ( Number ( box . items [ item . skuCode ] ) || 0 ) , 0 ) <= this . remaining ( item ) ) ; return ( this . palletCode . trim ( ) && this . palletCheck && Number ( this . palletLength ) > 0 && Number ( this . palletWidth ) > 0 && Number ( this . palletHeight ) > 0 && Number ( this . palletWeight ) > 0 && this . palletBoxCount > 0 ) && this . selectedItems . every ( ( item ) => this . quantity ( item . skuCode ) <= this . remaining ( item ) && ( this . quantity ( item . skuCode ) === 0 || this . boxCount ( item . skuCode ) > 0 ) ) ; } ,
unitLabel ( ) { return this . unitType === 'PALLET' ? '托盘码' : this . unitType === 'BOX' ? '箱号/箱唛' : '包裹码/快递单号' } ,
unitPlaceholder ( ) { return this . unitType === 'PALLET' ? '扫描托盘标签' : this . unitType === 'BOX' ? '扫描箱唛或录入箱号' : '扫描包裹面单或录入包裹码' }
, palletVolume ( ) { const l = Number ( this . palletLength ) || 0 ; const w = Number ( this . palletWidth ) || 0 ; const h = Number ( this . palletHeight ) || 0 ; return l > 0 && w > 0 && h > 0 ? ( l * w * h ) . toFixed ( 2 ) : '—' }
@@ -75,9 +93,24 @@
onHide ( ) { this . stopScanner ( ) } , onUnload ( ) { this . stopScanner ( ) } , beforeUnmount ( ) { this . stopScanner ( ) } ,
methods : {
goCount ( ) { const mark = this . result ? . labels ? . [ 0 ] ? . cartonMark ; if ( mark ) uni . navigateTo ( { url : '/pages/return-counting/return-counting?cartonMark=' + encodeURIComponent ( mark ) } ) } ,
selectUnitType ( type ) { if ( this . busy ) return ; this . unitType = type ; this . unitCode = '' ; this . boxRows = [ { items : { } } ] ; this . palletCheck = null ; this . activeTarget = type === 'PALLET' ? 'pallet' : 'unit' } ,
resizeBoxRows ( raw ) { const count = Math . floor ( Number ( raw ) || 0 ) ; if ( count < 1 || count > 100 ) return ; this . boxRows = Array . from ( { length : count } , ( _ , index ) => this . boxRows [ index ] || { items : { } } ) } ,
setBoxQuantity ( index , skuCode , raw ) { const value = Math . max ( 0 , Math . floor ( Number ( raw ) || 0 ) ) ; this . boxRows [ index ] . items = { ... this . boxRows [ index ] . items , [ skuCode ] : value } } ,
selectUnitType ( type ) { if ( this . busy ) return ; this . unitType = type ; this . unitCode = '' ; this . boxRows = [ { items : { } , length : '' , width : '' , height : '' , weight : '' , originalPackaging : true , confirmed : false } ] ; this . palletCheck = null ; this . activeTarget = type === 'PALLET' ? 'pallet' : 'unit' } ,
resizeBoxRows ( raw ) { if ( this . busy ) return ; const count = Math . floor ( Number ( raw ) || 0 ) ; if ( count < 1 || count > 100 ) return ; this . boxRows = Array . from ( { length : count } , ( _ , index ) => this . boxRows [ index ] || { items : { } , length : '' , width : '' , height : '' , weight : '' , originalPackaging : true , confirmed : false } ) } ,
setOriginalPackaging ( index , value ) { if ( this . busy ) return ; this . boxRows [ index ] . originalPackaging = value ; this . boxRows [ index ] . confirmed = false } ,
rowVolume ( box ) { const v = Number ( box ? . length ) * Number ( box ? . width ) * Number ( box ? . height ) ; return Number . isFinite ( v ) && v > 0 ? v . toFixed ( 2 ) : '—' } ,
setBoxQuantity ( index , skuCode , raw ) { if ( this . busy ) return ; this . boxRows [ index ] . items = { ... this . boxRows [ index ] . items , [ skuCode ] : raw } ; this . boxRows [ index ] . confirmed = false } ,
boxRemaining ( index , item ) { const allocated = this . boxRows . reduce ( ( sum , box , rowIndex ) => sum + ( rowIndex === index ? 0 : Math . max ( 0 , Number ( box . items [ item . skuCode ] ) || 0 ) ) , 0 ) ; return Math . max ( 0 , this . remaining ( item ) - allocated ) } ,
boxRowValid ( index ) {
const box = this . boxRows [ index ]
if ( ! box || ! [ 'length' , 'width' , 'height' , 'weight' ] . every ( field => Number . isFinite ( Number ( box [ field ] ) ) && Number ( box [ field ] ) > 0 ) || typeof box . originalPackaging !== 'boolean' ) return false
return this . selectedItems . some ( item => Number ( box . items [ item . skuCode ] ) > 0 ) && this . selectedItems . every ( item => { const quantity = Number ( box . items [ item . skuCode ] || 0 ) ; return Number . isSafeInteger ( quantity ) && quantity >= 0 && quantity <= this . boxRemaining ( index , item ) } )
} ,
editBoxRow ( index ) { if ( this . busy ) return ; this . boxRows [ index ] . confirmed = false ; this . error = null } ,
confirmBoxRow ( index ) {
if ( this . busy ) return
if ( ! this . boxRowValid ( index ) ) return this . setError ( new Error ( '请填写有效箱规、重量和正整数实收数量,数量不能超过扣除其他箱后的剩余量' ) , '本箱确认失败' )
this . boxRows [ index ] . confirmed = true ; this . error = null
if ( this . allBoxRowsConfirmed ) this . choosePhoto ( true )
} ,
acceptUnit ( raw ) { const code = String ( raw ? . detail ? . value ? ? raw ? ? '' ) . trim ( ) ; if ( ! code ) return ; this . unitCode = code ; if ( this . unitType !== 'PARCEL' ) this . palletCode = ` ${ this . unitType } : ${ code } ` ; this . activeTarget = this . unitType === 'PARCEL' ? 'unit' : 'tracking' ; playSuccessSound ( ) } ,
startScanner ( ) { this . stopScanner ( ) ; this . _stopScanner = startScanBroadcast ( { onScan : ( value ) => this . handleScan ( value ) , onError : ( error ) => this . setError ( error , '扫码广播注册失败' ) } ) } ,
stopScanner ( ) { if ( this . _stopScanner ) this . _stopScanner ( ) ; this . _stopScanner = null } ,
@@ -99,11 +132,11 @@
printPallet ( ) { if ( ! this . newPalletHtml ) return ; /* #ifdef H5 */ const view = window . open ( '' , '_blank' ) ; if ( view ) { view . document . write ( this . newPalletHtml ) ; view . document . close ( ) ; view . focus ( ) ; view . print ( ) } else this . setError ( new Error ( '请允许打开标签打印窗口' ) , '打印失败' ) ; /* #endif */ /* #ifndef H5 */ this . setError ( new Error ( '当前设备未配置打印服务' ) , '打印失败' ) ; /* #endif */ } ,
printReturnCarton ( label ) { if ( ! label ? . printableHtml ) return ; /* #ifdef H5 */ const view = window . open ( '' , '_blank' ) ; if ( view ) { view . document . write ( label . printableHtml ) ; view . document . close ( ) ; view . focus ( ) ; view . print ( ) } else this . setError ( new Error ( '请允许打开标签打印窗口' ) , '打印失败' ) ; /* #endif */ /* #ifndef H5 */ this . setError ( new Error ( '当前设备未配置打印服务' ) , '打印失败' ) ; /* #endif */ } ,
acceptTracking ( raw ) { this . trackingNo = String ( raw ? . detail ? . value ? ? raw ? ? '' ) . trim ( ) ; playSuccessSound ( ) } ,
async confirmReceipt ( ) { if ( this . busy ) return ; if ( this . receiptBatch ) { await this . run ( ( ) => this . resumeReceiptBatch ( ) , '退货入库失败' ) ; return } const totalBoxCount = this . palletBoxCount ; if ( this . unitType === 'PALLET' && totalBoxCount <= 0 ) { this . setError ( new Error ( '请先填写各 SKU 的箱数' ) , '托盘签收校验失败' ) ; return } if ( ! this . canConfirm || this . busy ) return ; const willComplete = this . current . expectedItems . every ( ( item ) => ( this . unitType !== 'PALLET' ? this . boxRows . reduce ( ( sum , box ) => sum + ( Number ( box . items [ item . skuCode ] ) || 0 ) , 0 ) : this . quantity ( item . skuCode ) ) >= this . remaining ( item ) ) ; if ( willComplete && ! this . photoFileIds . length ) { const modal = await new Promise ( ( resolve ) => uni . showModal ( { title : '缺少退货打托照片' , content : '本次签收将完成整单入库,请先拍照并上传后再确认打托。' , confirmText : '去拍照' , cancelText : '取消' , success : resolve } ) ) ; if ( modal ? . confirm ) this . error = { message : '请先拍照并上传退货打托照片后再确认' } ; return }
async confirmReceipt ( ) { if ( this . busy ) return ; if ( this . receiptBatch ) { await this . run ( ( ) => this . resumeReceiptBatch ( ) , '退货入库失败' ) ; return } const totalBoxCount = this . palletBoxCount ; if ( this . unitType === 'PALLET' && totalBoxCount <= 0 ) { this . setError ( new Error ( '请先填写各 SKU 的箱数' ) , '托盘签收校验失败' ) ; return } if ( ! this . canConfirm || this . busy || ( this . unitType !== 'PALLET' && ! this . allBoxRowsConfirmed ) ) return ; if ( this . unitType !== 'PALLET' && ! this . photoFileIds . length ) { this . setError ( new Error ( '请先拍照或上传签收照片,再确认签收入库' ) , '签收校验失败' ) ; return } const willComplete = this . current . expectedItems . every ( ( item ) => ( this . unitType !== 'PALLET' ? this . boxRows . reduce ( ( sum , box ) => sum + ( Number ( box . items [ item . skuCode ] ) || 0 ) , 0 ) : this . quantity ( item . skuCode ) ) >= this . remaining ( item ) ) ; if ( willComplete && ! this . photoFileIds . length ) { const modal = await new Promise ( ( resolve ) => uni . showModal ( { title : '缺少退货打托照片' , content : '本次签收将完成整单入库,请先拍照并上传后再确认打托。' , confirmText : '去拍照' , cancelText : '取消' , success : resolve } ) ) ; if ( modal ? . confirm ) this . error = { message : '请先拍照并上传退货打托照片后再确认' } ; return }
await this . run ( async ( ) => {
const rows = this . unitType !== 'PALLET' ? this . boxRows : [ null ]
const entries = rows . map ( ( box , index ) => {
const payload = { returnNo : this . current . returnNo , trackingNo : ( this . unitType === 'PARCEL' ? this . unitCode . trim ( ) : this . trackingNo . trim ( ) ) || undefined , startedBy : this . startedBy , unitType : this . unitType , unitCode : undefined , palletCode : this . unitType === 'PALLET' || this . unitType === 'BOX' ? this . palletCode . trim ( ) || undefined : undefined , length : this . unitType === 'PALLET' ? Number ( this . palletLength ) : undefined , width : this . unitType === 'PALLET' ? Number ( this . palletWidth ) : undefined , height : this . unitType === 'PALLET' ? Number ( this . palletHeight ) : undefined , weight : this . unitType === 'PALLET' ? Number ( this . palletWeight ) : undefined , totalBoxCount : this . unitType === 'PALLET' ? totalBoxCount : undefined , items : ( this . unitType !== 'PALLET' ? this . selectedItems . map ( ( item ) => ( { skuCode : item . skuCode , receivedQuantity : Number ( box . items [ item . skuCode ] ) || 0 , boxCount : this . unitType === 'BOX' ? 1 : undefined } ) ) : this . selectedItems . map ( ( item ) => ( { skuCode : item . skuCode , receivedQuantity : this . quantity ( item . skuCode ) , boxCount : this . boxCount ( item . skuCode ) } ) ) ) . filter ( ( item ) => item . receivedQuantity > 0 ) }
const payload = { returnNo : this . current . returnNo , trackingNo : ( this . unitType === 'PARCEL' ? this . unitCode . trim ( ) : this . trackingNo . trim ( ) ) || undefined , startedBy : this . startedBy , unitType : this . unitType , unitCode : this . unitType === 'PARCEL' || this . unitType === 'BOX' ? undefined : undefined , palletCode : this . unitType === 'PALLET' || this . unitType === 'BOX' ? this . palletCode . trim ( ) || undefined : undefined , length : this . unitType === 'PALLET' ? Number ( this . palletLength ) : Number ( box . length ) , width : this . unitType === 'PALLET' ? Number ( this . palletWidth ) : Number ( box . width ) , height : this . unitType === 'PALLET' ? Number ( this . palletHeight ) : Number ( box . height ) , weight : this . unitType === 'PALLET' ? Number ( this . palletWeight ) : Number ( box . weight ) , originalPackaging : this . unitType === 'PALLET' ? undefined : box . originalPackaging , totalBoxCount : this . unitType === 'PALLET' ? totalBoxCount : undefined , items : ( this . unitType !== 'PALLET' ? this . selectedItems . map ( ( item ) => ( { skuCode : item . skuCode , receivedQuantity : Number ( box . items [ item . skuCode ] ) || 0 , boxCount : this . unitType === 'BOX' ? 1 : undefined } ) ) : this . selectedItems . map ( ( item ) => ( { skuCode : item . skuCode , receivedQuantity : this . quantity ( item . skuCode ) , boxCount : this . boxCount ( item . skuCode ) } ) ) ) . filter ( ( item ) => item . receivedQuantity > 0 ) }
const key = warehouseIdempotencyKey ( 'returnReceipt' , ` confirm- ${ index } ` , payload )
return { request : { ... payload , idempotencyKey : key . value } , result : null }
} )
@@ -142,7 +175,22 @@
playSuccessSound ( )
} ,
manualComplete ( ) { if ( ! this . current || this . busy ) return ; uni . showModal ( { title : '缺货手动完结' , editable : true , placeholderText : '请填写实收少于预期数量的原因' , success : async ( modal ) => { if ( ! modal . confirm ) return ; const reason = String ( modal . content || '' ) . trim ( ) ; if ( ! reason ) { this . setError ( new Error ( '必须填写差异原因' ) , '手动完结失败' ) ; return } await this . run ( async ( ) => { await manuallyCompleteReturnReceipt ( this . current . returnNo , reason ) ; this . result = { palletCode : '' , receiptNo : '' , receiptBatchNo : '' , expectedQuantity : this . current . expectedItems . reduce ( ( sum , item ) => sum + item . expectedQuantity , 0 ) , receivedQuantity : this . current . expectedItems . reduce ( ( sum , item ) => sum + item . receivedQuantity , 0 ) , fullyMatched : false } ; this . current = null ; this . candidates = null ; this . activeTarget = 'lookup' ; playSuccessSound ( ) } , '手动完结失败' ) } } ) } ,
choosePhoto ( ) { if ( ! this . current || this . busy || this . photoFileIds . length >= 20 ) return ; const no = this . current . returnNo ; uni . chooseImage ( { count : Math . min ( 9 , 20 - this . photoFileIds . length ) , sourceType : [ 'camera' , 'album' ] , success : async ( { tempFilePaths , tempFiles } ) => { await this . run ( async ( ) => { for ( const [ index , path ] of ( tempFilePaths || [ ] ) . entries ( ) ) { if ( Number ( tempFiles ? . [ index ] ? . size || 0 ) > 10 * 1024 * 1024 ) throw new Error ( '照片不能超过 10MB' ) ; const file = await uploadReturnPalletPhoto ( no , path ) ; const id = String ( file . fileId ) ; this . photoFileIds . push ( id ) ; this . photoNames . push ( file . fileName || '退货打托照片' ) ; this . photoUrls [ id ] = path } playSuccessSound ( ) } , '退货打托照片上传失败' ) } } ) } ,
choosePhoto ( cameraOnly = false ) {
if ( ! this . current || this . busy ) return
if ( this . photoFileIds . length >= 20 ) return
const no = this . current . returnNo ; const draft = JSON . stringify ( this . boxRows ) ; const type = this . unitType
uni . chooseImage ( { count : cameraOnly ? 1 : Math . min ( 9 , 20 - this . photoFileIds . length ) , sourceType : cameraOnly ? [ 'camera' ] : [ 'camera' , 'album' ] , success : async ( { tempFilePaths , tempFiles } ) => {
if ( this . current ? . returnNo !== no || this . unitType !== type || JSON . stringify ( this . boxRows ) !== draft ) return
await this . run ( async ( ) => {
for ( const [ index , path ] of ( tempFilePaths || [ ] ) . entries ( ) ) {
if ( Number ( tempFiles ? . [ index ] ? . size || 0 ) > 10 * 1024 * 1024 ) throw new Error ( '照片不能超过 10MB' )
const file = await uploadReturnPalletPhoto ( no , path ) ; const id = String ( file . fileId )
this . photoFileIds . push ( id ) ; this . photoNames . push ( file . fileName || '退货打托照片' ) ; this . photoUrls [ id ] = path
}
playSuccessSound ( )
} , '退货打托照片上传失败' )
} } )
} ,
previewPhoto ( fileId ) { const urls = this . photoFileIds . map ( id => this . photoUrls [ id ] ) . filter ( Boolean ) ; if ( this . photoUrls [ fileId ] ) uni . previewImage ( { current : this . photoUrls [ fileId ] , urls } ) } ,
removePhoto ( index ) { const [ id ] = this . photoFileIds . splice ( index , 1 ) ; this . photoNames . splice ( index , 1 ) ; if ( id ) delete this . photoUrls [ id ] } ,
async reloadCurrent ( ) { if ( ! this . current ? . returnNo || this . busy ) return ; await this . openOrder ( this . current . returnNo , this . startedBy , false ) } ,
@@ -154,7 +202,7 @@
this . entryMode = 'SKU' ; this . lookupCode = '' ; this . selectedSku = ''
this . palletCheck = null ; this . newPalletHtml = ''
this . palletLength = '' ; this . palletWidth = '' ; this . palletHeight = '' ; this . palletWeight = ''
this . unitCode = '' ; this . boxRows = [ { items : { } } ]
this . unitCode = '' ; this . boxRows = [ { items : { } , length : '' , width : '' , height : '' , weight : '' , originalPackaging : true , confirmed : false } ]
this . photoFileIds = [ ] ; this . photoNames = [ ] ; this . photoUrls = { }
} ,
async nextReceipt ( ) {
@@ -164,7 +212,7 @@
await this . run ( async ( ) => {
await this . openOrderValue ( returnNo , 'SKU' )
this . palletLength = '' ; this . palletWidth = '' ; this . palletHeight = '' ; this . palletWeight = ''
this . unitCode = '' ; this . boxRows = [ { items : { } } ] ; this . newPalletHtml = ''
this . unitCode = '' ; this . boxRows = [ { items : { } , length : '' , width : '' , height : '' , weight : '' , originalPackaging : true , confirmed : false } ] ; this . newPalletHtml = ''
this . result = null
} , '读取剩余待签收货物失败' )
} ,
@@ -185,4 +233,6 @@
. unit - tabs { display : flex ; gap : 8 px ; margin : 10 px 0 } . unit - tab { flex : 1 ! important ; min - width : 0 ! important ; height : 34 px ! important ; min - height : 34 px ! important ; margin : 0 ! important ; padding : 0 10 px ! important ; border : 1 px solid # d6e1ef ! important ; border - radius : 8 px ! important ; background : # f7f9fc ! important ; color : # 53677 f ! important ; font - size : 13 px ! important ; line - height : 34 px ! important } . unit - tab . active { border - color : # 135 ee8 ! important ; background : # eaf2ff ! important ; color : # 135 ee8 ! important ; font - weight : 700 }
. dimension - grid { display : grid ; grid - template - columns : repeat ( 2 , 1 fr ) ; gap : 7 px ; margin - top : 10 px } . dimension - grid label > text { display : block ; margin - bottom : 3 px ; color : # 65768 b ; font - size : 10 px } . dimension - grid . number - input { box - sizing : border - box ; width : 100 % ; height : 34 px ; padding : 0 8 px ; border : 1 px solid # d7e1ef ; border - radius : 7 px ; color : # 24364 b ; font - size : 13 px ; text - align : center ; background : # fff } . dimension - grid . number - input : focus { border - color : # 2167 e8 ; box - shadow : 0 0 0 2 px rgba ( 33 , 103 , 232 , .12 ) } . volume - box { display : flex ; align - items : center ; justify - content : center ; gap : 4 px ; height : 34 px ; padding : 0 6 px ; border : 1 px solid # b9d3f4 ; border - radius : 7 px ; color : # 65768 b ; font - size : 10 px ; background : # f4f8ff } . volume - box strong { color : # 135 ee8 ; font - size : 13 px }
. work - card > . row - head . business - code { font - size : 16 px ; line - height : 20 px } . work - card > . row - head . meta - line { font - size : 10 px ; line - height : 15 px } . work - card > . row - head . status - pill { padding : 4 px 7 px ; font - size : 10 px }
. physical - unit - card { padding : 12 px ! important ; border : 1 px solid # d8e5f5 ! important ; background : linear - gradient ( 145 deg , # fff 0 % , # f7faff 100 % ) ; box - shadow : 0 4 px 12 px rgba ( 30 , 78 , 130 , .08 ) } . unit - card - head { display : flex ; align - items : center ; justify - content : space - between ; margin - bottom : 8 px } . unit - card - tag { padding : 3 px 7 px ; border - radius : 10 px ; background : # eaf2ff ; color : # 135 ee8 ; font - size : 10 px ; font - weight : 700 } . unit - dimension - grid { margin - top : 0 } . unit - volume { display : flex ; align - items : baseline ; justify - content : flex - end ; gap : 4 px ; margin - top : 7 px ; color : # 728197 ; font - size : 10 px } . unit - volume strong { color : # 135 ee8 ; font - size : 15 px } . packaging - choice { display : flex ; align - items : center ; gap : 7 px ; margin - top : 10 px ; padding - top : 9 px ; border - top : 1 px solid # e6edf6 ; color : # 52667 e ; font - size : 11 px } . packaging - choice > text { flex : 1 } . choice - button { width : 42 px ! important ; min - width : 42 px ! important ; height : 28 px ! important ; min - height : 28 px ! important ; margin : 0 ! important ; padding : 0 ! important ; border : 1 px solid # d1ddeb ! important ; border - radius : 7 px ! important ; background : # fff ! important ; color : # 65768 b ! important ; font - size : 11 px ! important ; line - height : 28 px ! important } . choice - button . selected { border - color : # 135 ee8 ! important ; background : # eaf2ff ! important ; color : # 135 ee8 ! important ; font - weight : 700 }
. receipt - quantity - heading { display : flex ; align - items : center ; flex - wrap : nowrap ; gap : 8 px ; margin - bottom : 10 px } . receipt - quantity - heading . card - title { margin - bottom : 0 ; white - space : nowrap } . unit - count - row { display : flex ; align - items : center ; gap : 6 px ; margin - left : auto ; flex - shrink : 0 ; white - space : nowrap ; color : # 65768 b ; font - size : 10 px } . unit - count - row . number - input { box - sizing : border - box ; width : 48 px ; min - width : 48 px ; height : 30 px ; min - height : 30 px ; margin : 0 ; padding : 0 4 px ; border - radius : 6 px ; text - align : center ; font - size : 13 px } . quantity - row . unit - quantity - input { width : 88 px ; flex - shrink : 0 ; font - size : 12 px } . physical - unit - card . quantity - row > text { min - width : 0 ; overflow - wrap : anywhere } . unit - card - head . link - button { margin : 0 ; padding : 0 ; font - size : 10 px }
< / style >