@@ -2,19 +2,20 @@
< view v-if = "user" class="page-shell portal-page work-page" >
< PortalHeader title = "退货入库" :user = "user" fallback -url = " / pages / module / module ? key = inbound " / >
< scroll-view class = "work-scroll" scroll -y > < view class = "portal-content work-content" >
< view v-if = "!current && !result" class="work-card" >
< view v-if = "!current && !result && !receiptBatch " class="work-card" >
< text class = "card-title" > 扫描 SKU 69 码 < / text >
< ScanTargetInput name = "lookup" v-model = "lookupCode" label="SKU 69 码" placeholder="扫描商品 69 码,自动带出退货单号" :active="activeTarget === 'lookup'" @activate="activeTarget = 'lookup'" @submit="lookup" / >
< text class = "scan-hint" > 扫描完成后自动匹配对应退货单 < / text >
< / view >
< view v-if = "!current && candidates !== null && !result" class="section-gap" >
< view v-if = "!current && candidates !== null && !result && !receiptBatch " class="section-gap" >
< view class = "row-head candidate-heading" > < text class = "card-title" > 匹配退货单 < / text > < text class = "meta-line" > { { candidates . length } } 个候选 < / text > < / view >
< view v-if = "!candidates.length" class="empty-box"><text class="empty-copy" > {{ entryMode = = = ' SKU ' ? ' 当前绑定仓没有包含该 SKU 的待签收退货单 ' : ' 当前绑定仓没有匹配的待签收或签收中退货单 ' }} < / text > < / view >
< view v-else class = "task-list" > < button v-for = "item in candidates" :key="item.returnNo" class="task-card candidate-card" @tap="selectCandidate(item)"><view class="row-head"><text class="business-code">{{ item.returnNo }}</text><text class="status-pill">{{ statusLabel(item.status) }}</text></view><text class="meta-line">客户参考号:{{ item.customerReferenceNo || '—' }}</text><view class="metric-row"><view class="metric"><strong>{{ item.skuReceivedQuantity }}/{{ item.skuExpectedQuantity }}</strong><text>该 SKU</text></view><view class="metric"><strong>{{ item.orderReceivedQuantity }}/{{ item.orderExpectedQuantity }}</strong><text>整单进度</text></view><text class="select-link" > 选择 › < / text > < / view > < / button > < / view >
< / view >
< template v-if = "current && !result" >
< view v-if = "receiptBatch && !result" class="work-card"><text class="card-title">待完成按箱签收</text><text class="business-code">{{ receiptBatch.returnNo }}</text><text class="hint">已确认 {{ receiptBatch.entries.filter(entry => entry.result).length }} / {{ receiptBatch.entries.length }} 箱。请继续本批签收,已确认的箱不会重新入库;本批内容暂不可修改。</text><button class="primary" :disabled="busy" @tap="confirmReceipt" > {{ busy ? ' 提交中 … ' : ' 继续本批签收 ' }} < / button > < / view >
< template v-if = "current && !result && !receiptBatch" >
< view class = "work-card" > < view class = "row-head" > < view > < text class = "business-code" > { { current . returnNo } } < / text > < text class = "meta-line" > { { startedBy === 'SKU' ? '通过 SKU 候选进入' : '通过退货单号进入' } } < / text > < / view > < text class = "status-pill" > { { statusLabel ( current . status ) } } < / text > < / view > < button class = "link-button" @tap ="clearCurrent" > 更换退货单 < / button > < / view >
< view class = "work-card" >
@@ -39,7 +40,7 @@
< view v-if = "result" class="success-box result-card"><text class="success-title">退货签收成功</text><view v-if="result.labels?.length"><view v-for="(label,index) in result.labels" :key="label.cartonMark" class="item-card"><text class="business-code">第 {{ index + 1 }} 箱 · {{ label.cartonMark }}</text><button class="secondary" @tap="printReturnCarton(label)">打印箱唛</button></view></view><view v-else-if="result.printableHtml" class="item-card"><text class="business-code">{{ result.unitType === 'PALLET' ? '托盘标签' : '箱唛' }} · {{ result.cartonMark }}</text><button class="secondary" @tap="printReturnCarton(result)">打印{{ result.unitType === 'PALLET' ? '托盘标签' : '箱唛' }}</button></view><text v-if="result.palletCode" class="result-carton">托盘:{{ result.palletCode }}</text><text class="meta-line">签收单:{{ result.receiptNo }}</text><text class="meta-line">计费批次:{{ result.receiptBatchNo }}</text><view class="metric-row"><view class="metric"><strong>{{ result.receivedQuantity }}/{{ result.expectedQuantity }}</strong><text>整单累计</text></view><view class="metric"><strong>{{ result.fullyMatched ? '已完成' : '签收中' }}</strong><text>退货单状态</text></view></view><button class="primary" @tap="nextReceipt" > 继续签收 < / button > < / view >
< view v-if = "error" class="error-box" role="alert"><text class="error-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><button v-if="current" class="secondary" :disabled="busy" @tap="reloadCurrent" > 刷新退货单 < / button > < / view >
< view v-if = "error" class="error-box" role="alert"><text class="error-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><button v-if="current && !receiptBatch " class="secondary" :disabled="busy" @tap="reloadCurrent" > 刷新退货单 < / button > < / view >
< / view > < / scroll -view >
< / view >
< / template >
@@ -49,7 +50,7 @@
import ScanTargetInput from '@/components/ScanTargetInput.vue'
import { checkReturnPallet , confirmReturnReceipt , createReturnPallet , downloadReturnPalletPhoto , findReturnSkuCandidates , manuallyCompleteReturnReceipt , startReturnReceipt , uploadReturnPalletPhoto } from '@/api/return-receipt.js'
import { session , requireSession } from '@/stores/session.js'
import { releaseWarehouseKey , resetWarehouseDomain , restoreWarehouseWork , updateWarehouseWork , warehouseIdempotencyKey , warehouseWork } from '@/stores/warehouse-work.js'
import { resetWarehouseDomain , restoreWarehouseWork , updateWarehouseWork , warehouseIdempotencyKey , warehouseWork } from '@/stores/warehouse-work.js'
import { startScanBroadcast } from '@/utils/scan-broadcast.js'
import { playErrorSound , playSuccessSound } from '@/utils/audio-feedback.js'
@@ -57,6 +58,7 @@
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 } } ,
computed : {
receiptBatch ( ) { return warehouseWork . returnReceipt . pendingBatch } ,
entryMode : { get ( ) { return warehouseWork . returnReceipt . entryMode } , set ( value ) { updateWarehouseWork ( 'returnReceipt' , { entryMode : value } ) } } ,
lookupCode : { get ( ) { return warehouseWork . returnReceipt . lookupCode } , set ( value ) { updateWarehouseWork ( 'returnReceipt' , { lookupCode : value } ) } } ,
startedBy : { get ( ) { return warehouseWork . returnReceipt . startedBy } , set ( value ) { updateWarehouseWork ( 'returnReceipt' , { startedBy : value } ) } } ,
@@ -71,7 +73,7 @@
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 ) : '—' }
} ,
onShow ( ) { if ( ! requireSession ( ) ) return ; this . user = session . currentUser ; restoreWarehouseWork ( ` ${ this . user . id } : ${ this . user . warehouseId } ` ) ; this . entryMode = 'SKU' ; this . startScanner ( ) ; if ( warehouseWork . returnReceipt . currentReturnNo && ! this . current && ! this . busy ) this . openOrder ( warehouseWork . returnReceipt . currentReturnNo , 'SKU' , false ) } ,
onShow ( ) { if ( ! requireSession ( ) ) return ; this . user = session . currentUser ; restoreWarehouseWork ( ` ${ this . user . id } : ${ this . user . warehouseId } ` ) ; this . entryMode = 'SKU' ; this . startScanner ( ) ; if ( ! this . receiptBatch && warehouseWork . returnReceipt . currentReturnNo && ! this . current && ! this . busy ) this . openOrder ( warehouseWork . returnReceipt . currentReturnNo , 'SKU' , false ) } ,
onHide ( ) { this . stopScanner ( ) } , onUnload ( ) { this . stopScanner ( ) } , beforeUnmount ( ) { this . stopScanner ( ) } ,
methods : {
selectUnitType ( type ) { if ( this . busy ) return ; this . unitType = type ; this . unitCode = '' ; this . boxRows = [ { items : { } } ] ; this . palletCheck = null ; this . activeTarget = type === 'PALLET' ? 'pallet' : 'unit' } ,
@@ -80,7 +82,7 @@
acceptUnit ( raw ) { const code = String ( raw ? . detail ? . value ? ? raw ? ? '' ) . trim ( ) ; if ( ! code ) return ; this . unitCode = code ; this . palletCode = ` ${ this . unitType } : ${ code } ` ; this . activeTarget = '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 } ,
handleScan ( value ) { if ( this . busy ) return ; if ( ! this . current ) { this . lookupCode = value ; this . lookup ( value ) ; return } if ( this . activeTarget === 'unit' ) this . acceptUnit ( value ) ; else this . acceptTracking ( value ) } ,
handleScan ( value ) { if ( this . busy || this . receiptBatch ) return ; if ( ! this . current ) { this . lookupCode = value ; this . lookup ( value ) ; return } if ( this . activeTarget === 'unit' ) this . acceptUnit ( value ) ; else this . acceptTracking ( value ) } ,
switchMode ( mode ) { if ( this . busy ) return ; this . entryMode = mode ; this . lookupCode = '' ; this . candidates = null ; this . error = null ; this . activeTarget = 'lookup' } ,
async lookup ( raw ) { const value = String ( raw ? . detail ? . value ? ? raw ? ? this . lookupCode ) . trim ( ) ; if ( ! value || this . busy ) return ; this . lookupCode = value ; this . error = null ; this . result = null ; if ( this . entryMode === 'SKU' ) await this . run ( async ( ) => { const response = await findReturnSkuCandidates ( value ) ; this . candidates = response . candidates ; this . selectedSku = response . skuCode ; if ( this . candidates . length === 1 ) await this . openOrderValue ( this . candidates [ 0 ] . returnNo , 'SKU' ) ; else if ( this . candidates . length ) playSuccessSound ( ) } , '查询 SKU 候选失败' ) ; else await this . openOrder ( value , 'RETURN_NO' ) } ,
async selectCandidate ( item ) { if ( ! item ? . returnNo || this . busy ) return ; this . selectedSku = this . lookupCode ; await this . openOrder ( item . returnNo , 'SKU' ) } ,
@@ -98,14 +100,54 @@
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 ( ) { 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 === 'BOX' ? 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 === 'BOX' ? this . boxRows : [ null ] ; const results = [ ] ; for ( const [ index , box ] of rows . entries ( ) ) { const payload = { returnNo : this . current . returnNo , trackingNo : this . trackingNo . trim ( ) || undefined , startedBy : this . startedBy , unitType : this . unitType , unitCode : this . unitType === 'PARCEL' ? this . unitCode . trim ( ) : 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 === 'BOX' ? this . selectedItems . map ( ( item ) => ( { skuCode : item . skuCode , receivedQuantity : Number ( box . items [ item . skuCode ] ) || 0 , boxCount : 1 } ) ) : this . selectedItems . map ( ( item ) => ( { skuCode : item . skuCode , receivedQuantity : this . quantity ( item . skuCode ) , boxCount : this . unitType === 'PALLET' ? this . boxCount ( item . skuCode ) : undefined } ) ) ) . filter ( ( item ) => item . receivedQuantity > 0 ) } ; const key = warehouseIdempotencyKey ( 'returnReceipt' , ` confirm- ${ index } ` , payload ) ; results . push ( await confirmReturnReceipt ( { ... payload , idempotencyKey : key . value } ) ) ; releaseWarehouseKey ( 'returnReceipt' , key . signature ) } const last = results [ results . length - 1 ] ; this . result = { ... last , labels : results . filter ( item => item . unitType === 'BOX' && item . printableHtml ) . map ( item => ( { cartonMark : item . cartonMark , printableHtml : item . printableHtml } ) ) } ; resetWarehouseDomain ( 'returnReceipt' ) ; this . current = null ; this . candidates = null ; this . activeTarget = 'lookup' ; 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 === 'BOX' ? 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 === 'BOX' ? this . boxRows : [ null ]
const entries = rows . map ( ( box , index ) => {
const payload = { returnNo : this . current . returnNo , trackingNo : this . trackingNo . trim ( ) || undefined , startedBy : this . startedBy , unitType : this . unitType , unitCode : this . unitType === 'PARCEL' ? this . unitCode . trim ( ) : 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 === 'BOX' ? this . selectedItems . map ( ( item ) => ( { skuCode : item . skuCode , receivedQuantity : Number ( box . items [ item . skuCode ] ) || 0 , boxCount : 1 } ) ) : this . selectedItems . map ( ( item ) => ( { skuCode : item . skuCode , receivedQuantity : this . quantity ( item . skuCode ) , boxCount : this . unitType === 'PALLET' ? this . boxCount ( item . skuCode ) : undefined } ) ) ) . filter ( ( item ) => item . receivedQuantity > 0 ) }
const key = warehouseIdempotencyKey ( 'returnReceipt' , ` confirm- ${ index } ` , payload )
return { request : { ... payload , idempotencyKey : key . value } , result : null }
} )
if ( this . unitType === 'BOX' ) {
updateWarehouseWork ( 'returnReceipt' , { pendingBatch : JSON . parse ( JSON . stringify ( { returnNo : this . current . returnNo , entries } ) ) } )
await this . resumeReceiptBatch ( )
} else {
this . finishReceipt ( [ await confirmReturnReceipt ( entries [ 0 ] . request ) ] )
}
} , '退货入库失败' )
} ,
async resumeReceiptBatch ( ) {
const batch = this . receiptBatch
const scopeKey = warehouseWork . scopeKey
if ( ! batch ) return
for ( let index = 0 ; index < batch . entries . length ; index ++ ) {
if ( warehouseWork . scopeKey !== scopeKey ) throw new Error ( '作业账号或仓库已改变,请重新进入作业' )
if ( batch . entries [ index ] . result ) continue
// Persist the immutable request before every attempt; storage failures must stop submission.
updateWarehouseWork ( 'returnReceipt' , { pendingBatch : batch } )
const result = await confirmReturnReceipt ( batch . entries [ index ] . request )
if ( warehouseWork . scopeKey !== scopeKey ) throw new Error ( '作业账号或仓库已改变,请重新进入作业' )
if ( ! result ) throw new Error ( '服务端未返回签收结果,请重试本批签收' )
batch . entries [ index ] . result = result
updateWarehouseWork ( 'returnReceipt' , { pendingBatch : batch } )
}
this . finishReceipt ( batch . entries . map ( entry => entry . result ) )
} ,
finishReceipt ( results ) {
const last = results [ results . length - 1 ]
this . result = { ... last , labels : results . filter ( item => item . unitType === 'BOX' && item . printableHtml ) . map ( item => ( { cartonMark : item . cartonMark , printableHtml : item . printableHtml } ) ) }
resetWarehouseDomain ( 'returnReceipt' )
this . current = null
this . candidates = null
this . activeTarget = 'lookup'
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 ( ) } , '退货打托照片上传失败' ) } } ) } ,
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 ) } ,
clearCurrent ( ) { resetWarehouseDomain ( 'returnReceipt' ) ; this . current = null ; this . candidates = null ; this . error = null ; this . activeTarget = 'lookup' } ,
clearCurrent ( ) { if ( this . busy || this . receiptBatch ) return ; resetWarehouseDomain ( 'returnReceipt' ) ; this . current = null ; this . candidates = null ; this . error = null ; this . activeTarget = 'lookup' } ,
nextReceipt ( ) { this . result = null ; this . clearCurrent ( ) } ,
async run ( work , fallback ) { if ( this . busy ) return ; this . busy = true ; this . error = null ; try { await work ( ) } catch ( error ) { this . setError ( error , fallback ) } finally { this . busy = false } } ,
setError ( error , fallback ) { this . error = { message : error ? . message || fallback , code : error ? . code , requestId : error ? . requestId , traceId : error ? . traceId } ; playErrorSound ( ) } ,