@@ -5,12 +5,14 @@
< view class = "work-heading" > < text class = "work-title" > { { title } } < / text > < text class = "work-subtitle" > { { stageSubtitle } } < / text > < / view >
< view v-if = "stage === 'execute'" class="work-card"><button class="secondary" :disabled="busy" @tap="goToClaimList" > 返回换标列表 < / button > < / view >
< view class = "row-head" > < text class = "card-title" > { { candidateHeading } } < / text > < button v-if = "stage !== 'review'" class="link-button" :disabled="loading" @tap="loadCandidates" > {{ loading ? ' 刷新中 … ' : ' 刷新 ' }} < / button > < / view >
< template v-if = "stage !== 'execute' || !showCurrent" >
< view class = "row-head" > < text class = "card-title" > { { candidateHeading } } < / text > < button v-if = "stage !== 'review'" class="link-button" :disabled="loading" @tap="bootstrap" > {{ loading ? ' 刷新中 … ' : ' 刷新 ' }} < / button > < / view >
< view v-if = "loading && !visibleCandidates.length" class="empty-box"><uni-load-more status="loading" /><text class="empty-copy" > 正在加载换标任务 … < / text > < / view >
< view v-else-if = "!visibleCandidates.length" class="empty-box"><uni-icons type="info" color="#8090a3" size="32" /><text class="empty-copy" > {{ emptyText }} < / text > < / view >
< view v-else-if = "stage !== 'review'" class="task-list"><view v-for="item in visibleCandidates" :key="item.relabelNo" class="task-card" :class="{ current: current?.relabelNo === item.relabelNo }" @tap="openExecution(item)"><view class="row-head"><view><text class="business-code">{{ item.relabelNo }}</text><text class="meta-line">客户订单:{{ item.customerOrderNo || '—' }}</text><text class="meta-line">换标类型:{{ relabelTypeName(item.operationType) }}</text></view><text class="status-pill">{{ statusLabel(item.taskStatus || item.applicationStatus) }}</text></view><view class="metric-row"><view class="metric"><strong>{{ item.expectedPackages }}</strong><text>应处理</text></view><view class="metric"><strong>{{ item.relabelledPackages }}</strong><text>已换标</text></view><view class="metric"><strong>{{ item.skippedPackages + item.exceptionPackages }}</strong><text>跳过/异常</text></view><text class="link-button" > 执行 › < / text > < / view > < / view > < / view >
< view v-else class = "task-list" > < view v-for = "item in visibleCandidates" :key="item.relabelNo" class="task-card"><view class="row-head"><view><text class="business-code">{{ item.relabelNo }}</text><text class="meta-line">客户订单:{{ item.customerOrderNo || '—' }}</text><text class="meta-line">换标类型:{{ relabelTypeName(item.operationType) }}</text></view><text class="status-pill">{{ statusLabel(item.taskStatus || item.applicationStatus) }}</text></view><view class="metric-row"><view class="metric"><strong>{{ item.expectedPackages }}</strong><text>应处理</text></view><view class="metric"><strong>{{ item.relabelledPackages }}</strong><text>已换标</text></view><view class="metric" > < strong > { { item . skippedPackages + item . exceptionPackages } } < / strong > < text > 跳过 / 异常 < / text > < / view > < / view > < / view > < / view >
< / template >
< view v-if = "showCurrent" class="work-card" >
< text class = "status-pill" > 换标类型 : { { relabelTypeName ( current . operationType ) } } < / text >
< view class = "row-head" > < view > < text class = "business-code" > { { current . relabelNo } } < / text > < text class = "meta-line" > 客户订单 : { { current . customerOrderNo || '—' } } < / text > < text class = "meta-line" > 标签截止 : { { formatTime ( current . labelDeadline ) } } < / text > < / view > < text class = "status-pill" > { { statusLabel ( current . taskStatus || current . applicationStatus ) } } < / text > < / view >
@@ -22,28 +24,26 @@
< template v-else >
< view class = "tab-row" role = "tablist" > < button v-for = "tab in availableTabs" :key="tab.key" class="tab-button" role="tab" :aria-selected="taskTab === tab.key" :class="{ active: taskTab === tab.key }" :disabled="busy" @tap="changeTab(tab.key)" > {{ tab.name }} < / button > < / view >
< text class = "card-title section-gap" > { { taskTab === 'sku' ? 'SKU 换标明细' : '箱唛换标明细' } } < / text >
< view v-if = "taskTab === 'carton'" class="section-gap"><ScanTargetInput name="quickCarton" v-model="quickCarton" label="仓库唛头" placeholder="扫描或输入箱唛" :active="activeTarget === 'quick-carton'" @activate="activeTarget = 'quick-carton'" @submit="submitCarton" /><button class="primary" :disabled="busy || !quickCarton.trim()" @tap="submitCarton(quickCarton)">确认本箱</button><view v-if="lastCarton" class="success-box"><text class="meta-line">仓库唛头:{{ lastCarton.cartonMark }}</text><text class="card-title" > 请贴 FBA 箱唛 : {{ lastCarton.fbaCartonMark }} < / text > < / view > < / view >
< view v-if = "!visibleItems.length" class="empty-box"><text class="empty-copy" > {{ taskTab = = = ' sku ' ? ' 本单没有 SKU 换标明细 ' : ' 本单没有箱唛换标明细 ' }} < / text > < / view >
< view v-else class = "item-list" > < view v-for = "item in visibleItems" :key="item.itemId " c lass="item-card" :class="{ done: remaining(item) === 0, current: selectedItem?.itemId === item.itemId }" @tap="selectItem(item)" >
< view class = "row-head" > < text class = "business-code" > 仓库唛头 : { { item . cartonMark || '—' } } < / text > < text class = "status-pill" : class = "{ good: remaining(item) === 0 } "> 待处理 { { remaining ( item ) } } 箱 < / text > < / view >
< template v-if = "taskTab === 'sku'"><view v-for="(sku, index) in skuDetails(item)" :key="index" class="sku-detail"><text class="meta-line">旧 SKU: {{ sku.skuCode || '—' }}</text><text class="meta-line">新 SKU: {{ sku.newSkuCode || '—' }}</text><text class="meta-line" > 数量 : {{ sku.quantity = = null ? ' — ' : sku.quantity }} 件 < / text > < / view > < / template >
< template v-else > < text class = "meta-line "> 新 FBA 箱唛 : { { item . fbaCartonMark || '—' } } < / text > < text class = "meta-line" > 箱数 : { { item . expectedPackages } } < / text > < / template >
< text v-if = "item.productNameZh" class="meta-line">{{ item.productNameZh }}</text><text v-if="item.labelRequirement" class="meta-line" > {{ item.labelRequirement }} < / text >
< view v-if = "taskTab === 'carton'" class="section-gap"><ScanTargetInput name="quickCarton" v-model="quickCarton" label="仓库唛头" placeholder="扫描或输入箱唛" :active="activeTarget === 'quick-carton'" @activate="activeTarget = 'quick-carton'" @submit="submitCarton" /><button class="primary" :disabled="busy || !quickCarton.trim()" @tap="submitCarton(quickCarton)" > 确认本箱 < / button > < / view >
< view v-if = "taskTab === 'sku'" class="section-gap" >
< ScanTargetInput name = "quickSku" v-model = "quickSku" label="旧 SKU 标签 " p laceholder="扫描或输入旧 SKU 标签" :active="activeTarget === 'quick-sku'" @activate="activeTarget = 'quick-sku'" @submit="submitSku" / >
< button class = "primary" : disabled = "busy || !quickSku.trim()" @tap ="submitSku(quickSku) "> 确认 SKU < / button >
< text v-if = "skuChoices.length" class="hint" > 同一旧 SKU 有多条明细 , 请核对实际箱选择本次作业 : < / text >
< button v-for = "row in skuChoices" :key="`${row.item.itemId}:${row.index}`" :disabled="busy" @tap="commitSku(row) "> {{ row.item.cartonMark }} : {{ row.sku.skuCode }} 换标 {{ row.sku.newSkuCode }} · {{ row.sku.quantity }} 件 < / button >
< view v-for = "row in pendingSkuRows" :key="`${row.item.itemId}:${row.index}`" class="item-card"><text class="business-code">{{ row.sku.skuCode }} 换标 {{ row.sku.newSkuCode }}</text><text class="meta-line">{{ row.item.cartonMark }} · {{ row.sku.quantity }} 件</text><button class="link-button" @tap="selectItem(row.item); changeAction('EXCEPTION')" > 异常 / 跳过 < / button > < / view >
< / view >
< view v-if = "taskTab === 'carton'" class="item-list"><view v-for="item in visibleItems" :key="item.itemId" class="item-card" :class="{ done: remaining(item) === 0, current: selectedItem?.itemId === item.itemId }" @tap="selectItem(item)" >
< view class = "row-head" > < text class = "business-code" > { { item . cartonMark || '—' } } → { { item . fbaCartonMark || '—' } } < / text > < / view >
< text class = "meta-line" > FBA 目的仓 : { { item . destinationFbaWarehouseCode || '待维护' } } < / text >
< / view > < / view >
< template v-if = "taskTab === 'sku' && selectedItem && remaining(selectedItem) > 0" >
< template v-if = "taskTab === 'sku' && selectedItem && action !== 'RELABEL' && remaining(selectedItem) > 0" >
< view class = "mode-row section-gap" > < button v-for = "mode in actionModes" :key="mode.key" class="mode-button" :class="{ active: action === mode.key }" @tap="changeAction(mode.key)" > {{ mode.name }} < / button > < / view >
< ScanTargetInput name = "oldLabel" v-model = "oldLabel" label="仓库唛头" placeholder="扫描或输入仓库唛头" :active="activeTarget === 'old'" @activate="activeTarget = 'old'" @submit="handleOldLabel" / >
< template v-if = "taskTab === 'sku' && action === 'RELABEL'"><view v-for="(sku, index) in unitDraft.skuVerifications" :key="index" class="sku-detail" >
< text class = "meta-line" > { { skuDetails ( selectedItem ) [ index ] ? . skuCode } } → { { skuDetails ( selectedItem ) [ index ] ? . newSkuCode } } · { { sku . quantity } } 件 < / text >
< ScanTargetInput :name = "`sku-old-${index}`" v-model = "sku.skuCode" label="旧 SKU" placeholder="扫描或输入旧 SKU" :active="activeTarget === `sku-old-${index}`" @activate="activeTarget = `sku-old-${index}`" @submit="activeTarget = `sku-new-${index}`" / >
< ScanTargetInput :name = "`sku-new-${index}`" v-model = "sku.newSkuCode" label="新 SKU" placeholder="扫描或输入新 SKU" :active="activeTarget === `sku-new-${index}`" @activate="activeTarget = `sku-new-${index}`" / >
< / view > < / template >
< ScanTargetInput v-if = "taskTab === 'carton' && requiresNewCartonLabel" name="newLabel" v-model="newLabel" label="新 FBA 箱唛" placeholder="扫描或输入新 FBA 箱唛" :active="activeTarget === 'new'" @activate="activeTarget = 'new'" @submit="handleNewLabel" / >
< text v-if = "action === 'RELABEL' && selectedItem.operationType === 'CARTON_AND_SKU'" class="hint" > 箱唛作业 : {{ selectedItem.cartonProcessedPackages > 0 ? '已完成' : '待处理' } } · SKU 作业 : { { selectedItem . skuProcessedPackages > 0 ? '已完成' : '待处理' } } < / text >
< template v-if = "action !== 'RELABEL'"><textarea v-model="reason" class="reason-input section-gap" maxlength="1000" placeholder="必填:跳过或异常原因" /><view v-if="action === 'EXCEPTION'" class="mode-row section-gap"><button v-for="item in exceptionCodes" :key="item.key" class="mode-button" :class="{ active: exceptionCode === item.key }" @tap="exceptionCode = item.key" > {{ item.name }} < / button > < / view > < / template >
< button class = "primary" : disabled = "busy || !canSubmitUnit" @tap ="submitUnit" > {{ busyAction = = = ' unit ' ? ' 提交中 … ' : unitActionText }} < / button >
< / template >
< view class = "section-gap" >
< RelabelPalletWork v-if = "taskTab === 'carton'" ref="palletWork" :key="current.relabelNo" :task="current" @activate="activeTarget = 'pallet-work'" @busy="busy = $event" @saved="reloadCurrent" / >
< view class = "row-head" > < text class = "card-title" > 完成照片 < / text > < text class = "status-pill" > { { photoFileIds . length } } / 20 张 < / text > < / view >
< button class = "secondary" : disabled = "busy || photoFileIds.length >= 20" @tap ="choosePhoto" > 拍照或从相册选择 < / button >
< view class = "photo-grid" > < view v-for = "(fileId,index) in photoFileIds" :key="fileId" class="photo-tile" >
@@ -53,7 +53,8 @@
< view class = "photo-actions" > < button class = "link-button" :disabled = "busy" @tap ="retakePhoto(index)" > 重拍 < / button > < button class = "link-button" :disabled = "busy" @tap ="removePhoto(index)" > 移除 < / button > < / view >
< / view > < / view >
< textarea v-model = "remark" class="reason-input section-gap" maxlength="900" :placeholder="allProcessed ? '完成备注(选填)' : '必填:未全部扫描完成的仓库异常作业备注'" / >
< button class = "primary" : disabled = "busy || !photoFileIds.length || (!allProcessed && !remark.trim())" @tap ="completeTask" > {{ busyAction = = = ' complete ' ? ' 提交中 … ' : allProcessed ? ' 完成现场换标并等待复核 ' : ' 手动完成并登记异常 ' }} < / button >
< view v-if = "completionBlockReason" class="error-box section-gap"><text class="error-title" > 还不能完成换标 < / text > < text > { { completionBlockReason } } < / text > < / view >
< button class = "primary" : disabled = "busy || Boolean(completionBlockReason)" @tap ="completeTask" > {{ busyAction = = = ' complete ' ? ' 提交中 … ' : allProcessed ? ' 完成现场换标并等待复核 ' : ' 手动完成并登记异常 ' }} < / button >
< / view >
< / template >
< / template >
@@ -68,21 +69,22 @@
< script >
import PortalHeader from '@/components/PortalHeader.vue'
import RelabelPalletWork from '@/components/RelabelPalletWork.vue'
import ScanTargetInput from '@/components/ScanTargetInput.vue'
import { claimRelabelTask , completeRelabelTask , getRelabelTask , listRelabelCandidates , relabelUnit , reportRelabelException , reviewRelabelTask , skipRelabelUnit , uploadRelabelPhoto } from '@/api/relabel.js'
import { session , requireSession } from '@/stores/session.js'
import { releaseWarehouseKey , restoreWarehouseWork , updateWarehouseWork , warehouseIdempotencyKey , warehouseWork } from '@/stores/warehouse-work.js'
import { startScanBroadcast } from '@/utils/scan-broadcast.js'
import { scanRelabelCarton , listRelabelDocuments , downloadRelabelDocument , downloadRelabelPhoto } from '@/api/relabel.js'
import { scanRelabelSku , scanRelabelCarton , listRelabelDocuments , downloadRelabelDocument , downloadRelabelPhoto } from '@/api/relabel.js'
import { playErrorSound , playSuccessSound } from '@/utils/audio-feedback.js'
const STAGES = { relabelReceive : 'receive' , relabelExecute : 'execute' , relabelReview : 'review' }
const isConflict = ( error ) => [ '409' , 'A0409' , 'B0409' ] . includes ( String ( error ? . code || '' ) )
export default {
components : { PortalHeader , ScanTargetInput } ,
components : { PortalHeader , ScanTargetInput , RelabelPalletWork } ,
data ( ) { return { user : session . currentUser , stage : 'receive' , unitDrafts : { } , taskTab : 'sku' , candidates : [ ] , current : null , selectedItem : null , activeTarget : 'task' , loading : false , busy : false , busyAction : '' , error : null , successMessage : '' , exceptionCode : 'OTHER' , actionModes : [ { key : 'RELABEL' , name : '正常换标' } , { key : 'SKIP' , name : '跳过' } , { key : 'EXCEPTION' , name : '异常' } ] , exceptionCodes : [ { key : 'DAMAGED' , name : '破损' } , { key : 'MISSING_LABEL' , name : '缺标签' } , { key : 'LABEL_MISMATCH' , name : '标签不符' } , { key : 'UNREADABLE' , name : '无法识别' } , { key : 'OTHER' , name : '其他' } ] } } ,
computed : {
showCurrent ( ) { if ( ! this . current || this . stage === 'review' ) return false ; if ( this . stage === 'execute' ) return this . current . taskStatus === 'PROCESSING' && this . current . claimedByCurrentOperator ; return ! this . current . taskStatus || this . current . taskStatus === 'AVAILABLE' || ( this . current . taskStatus === 'PROCESSING' && this . current . claimedByCurrentOperator ) } ,
showCurrent ( ) { if ( ! this . current || this . stage === 'review' ) return false ; const owned = this . candidates . some ( ( item ) => item . relabelNo === this . current . relabelNo && item . taskStatus === 'PROCESSING' && item . claimedByCurrentOperator === true ) ; if ( this . stage === 'execute' ) return this . current . taskStatus === 'PROCESSING' && owned ; return ! this . current . taskStatus || this . current . taskStatus === 'AVAILABLE' || ( this . current . taskStatus === 'PROCESSING' && owned ) } ,
stageSubtitle ( ) { return this . stage === 'review' ? '仅展示当前仓库已完成现场换标并等待复核的单据' : this . stage === 'execute' ? '仅展示当前账号已认领且正在处理的换标任务' : '点击任意换标单直接进入执行;同一账号必须先完成当前任务才能处理其他换标单' } ,
availableTabs ( ) { const items = this . current ? . items || [ ] ; return [ { key : 'sku' , name : 'SKU 换标' , types : [ 'SKU_LABEL' , 'CARTON_AND_SKU' ] } , { key : 'carton' , name : '箱唛换标' , types : [ 'CARTON_LABEL' , 'CARTON_AND_SKU' ] } ] . filter ( ( tab ) => items . some ( ( item ) => tab . types . includes ( item . operationType ) ) ) } ,
unitDraft ( ) { return this . unitDrafts ? . [ this . selectedItem ? . itemId ] || { cartonMark : '' , fbaCartonMark : '' , skuVerifications : [ ] } } ,
@@ -92,7 +94,8 @@
candidateHeading ( ) { return this . stage === 'review' ? '待复核任务' : this . stage === 'execute' ? '处理中任务' : '换标任务' } ,
emptyText ( ) { return ` 暂无 ${ this . candidateHeading } ` } ,
visibleCandidates ( ) { const candidates = this . candidates ; if ( this . stage === 'review' ) return candidates . filter ( ( item ) => item . taskStatus === 'WAIT_REVIEW' ) ; if ( this . stage === 'execute' ) return candidates . filter ( ( item ) => item . taskStatus === 'PROCESSING' && item . claimedByCurrentOperator ) ; return candidates . filter ( ( item ) => ! item . taskStatus || item . taskStatus === 'AVAILABLE' || ( item . taskStatus === 'PROCESSING' && item . claimedByCurrentOperator ) ) } ,
visibleItems ( ) { return ( this . current ? . items || [ ] ) . filter ( ( item ) => this . taskTab === 'sku' ? [ 'SKU_LABEL' , 'CARTON_AND_SKU' ] . includes ( item . operationType ) : [ 'CARTON_LABEL' , 'CARTON_AND_SKU' ] . includes ( item . operationType ) ) } ,
visibleItems ( ) { return ( this . current ? . items || [ ] ) . filter ( ( item ) => ( this . taskTab === 'sku' ? [ 'SKU_LABEL' , 'CARTON_AND_SKU' ] . includes ( item . operationType ) : [ 'CARTON_LABEL' , 'CARTON_AND_SKU' ] . includes ( item . operationType ) ) && this . remaining ( item ) > 0 ) } ,
pendingSkuRows ( ) { return this . visibleItems . flatMap ( item => this . skuDetails ( item ) . map ( ( sku , index ) => ( { item , sku , index } ) ) . filter ( row => ! this . skuDone ( row . item . itemId , row . index ) ) ) } ,
requiresNewCartonLabel ( ) { return this . action === 'RELABEL' && this . selectedItem && this . selectedItem . operationType !== 'SKU_LABEL' } ,
taskCode : { get ( ) { return warehouseWork . relabel . currentNo } , set ( value ) { updateWarehouseWork ( 'relabel' , { currentNo : value } ) } } ,
oldLabel : { get ( ) { return this . unitDraft . cartonMark } , set ( value ) { if ( this . selectedItem ) this . unitDraft . cartonMark = value } } ,
@@ -102,11 +105,25 @@
remark : { get ( ) { return warehouseWork . relabel . remark } , set ( value ) { updateWarehouseWork ( 'relabel' , { remark : value } ) } } ,
photoFileIds ( ) { return warehouseWork . relabel . photoFileIds || [ ] } , photoNames ( ) { return warehouseWork . relabel . photoNames || [ ] } ,
allProcessed ( ) { return Boolean ( this . current ? . expectedPackages ) && this . current . relabelledPackages + this . current . skippedPackages + this . current . exceptionPackages === this . current . expectedPackages } ,
completionBlockReason ( ) {
if ( ! this . current ) return '请先选择换标任务'
if ( ! this . allProcessed && ! this . remark . trim ( ) ) return '仍有未扫描明细,请继续处理;需要异常完工时必须填写异常作业备注'
const requiresPalletBinding = ( this . current . items || [ ] ) . some ( ( item ) => [ 'CARTON_LABEL' , 'CARTON_AND_SKU' ] . includes ( item . operationType ) )
if ( requiresPalletBinding && Number ( this . current . relabelledPackages || 0 ) > 0 ) {
const round = this . current . reviewedAt || 'INITIAL'
const marks = new Set ( ( this . current . history || [ ] ) . filter ( ( entry ) => entry . action === 'PALLET_BIND' ) . flatMap ( ( entry ) => {
try { const detail = JSON . parse ( entry . detail ) ; return detail . round === round ? detail . cartonMarks || [ ] : [ ] } catch { return [ ] }
} ) )
if ( marks . size < Number ( this . current . relabelledPackages ) ) return '请先在“换标后组托”中完成本轮已换标箱的拆托合托绑定'
}
if ( ! this . photoFileIds . length ) return '请至少上传 1 张换标完成照片'
return ''
} ,
canSubmitUnit ( ) { if ( ! this . selectedItem || ! this . visibleItems . some ( ( item ) => item . itemId === this . selectedItem . itemId ) || ! this . oldLabel . trim ( ) ) return false ; if ( this . action === 'RELABEL' ) return this . cartonVerified && this . skuVerified ; return Boolean ( this . reason . trim ( ) ) } ,
unitActionText ( ) { return ( { RELABEL : '确认本箱换标完成' , SKIP : '确认跳过本箱' , EXCEPTION : '登记本箱异常' } ) [ this . action ] }
} ,
onLoad ( options ) { this . stage = STAGES [ options . op ] || 'receive' } ,
mixins : [ { data ( ) { return { documents : [ ] , quickCarton : '' , lastCarton : null , photoUrls : { } , photoErrors : { } } } } ] ,
mixins : [ { data ( ) { return { documents : [ ] , quickCarton : '' , lastCarton : null , quickSku : '' , lastSku : '' , skuChoices : [ ] , photoUrls : { } , photoErrors : { } } } } ] ,
watch : {
'current.reviewedAt' ( value ) { if ( this . current ? . reviewDecision === 'REWORK' && warehouseWork . relabel . reworkRound !== ` ${ this . current . relabelNo } : ${ value } ` ) { this . unitDrafts = { } ; this . selectedItem = null ; this . lastCarton = null ; this . quickCarton = '' ; updateWarehouseWork ( 'relabel' , { reworkRound : ` ${ this . current . relabelNo } : ${ value } ` , photoFileIds : [ ] , photoNames : [ ] , remark : '' } ) } } ,
photoFileIds : { deep : true , handler ( ids ) { for ( const id of ids ) if ( ! this . photoUrls [ id ] ) this . loadPhoto ( id ) } } ,
@@ -115,6 +132,30 @@
onShow ( ) { if ( ! requireSession ( ) ) return ; this . user = session . currentUser ; restoreWarehouseWork ( ` ${ this . user . id } : ${ this . user . warehouseId } ` ) ; if ( this . stage === 'execute' ) this . startScanner ( ) ; this . bootstrap ( ) } ,
onHide ( ) { this . stopScanner ( ) } , onUnload ( ) { this . stopScanner ( ) } , beforeUnmount ( ) { this . stopScanner ( ) } ,
methods : {
skuDone ( itemId , index ) { return ( this . current ? . history || [ ] ) . some ( entry => { if ( entry . action !== 'SKU_SCAN' ) return false ; try { const data = JSON . parse ( entry . detail ) ; return String ( data . itemId ) === String ( itemId ) && data . skuIndex === index && data . round === ( this . current . reviewedAt || 'INITIAL' ) } catch { return false } } ) } ,
async submitSku ( raw ) {
const oldSku = String ( raw ? . detail ? . value ? ? raw ? ? this . quickSku ) . trim ( )
if ( ! oldSku || this . busy || ! this . current ) return
this . quickSku = oldSku
const matches = this . pendingSkuRows . filter ( row => row . sku . skuCode === oldSku )
this . skuChoices = [ ]
if ( ! matches . length ) return this . setError ( new Error ( '该旧 SKU 不在待处理明细中,或已处理完成' ) )
if ( matches . length > 1 ) { this . skuChoices = matches ; return }
await this . commitSku ( matches [ 0 ] )
} ,
async commitSku ( row ) {
if ( this . busy || this . quickSku . trim ( ) !== row . sku . skuCode ) return
const payload = { itemId : row . item . itemId , skuIndex : row . index , oldSku : this . quickSku . trim ( ) , expectedVersion : this . current . version }
const key = warehouseIdempotencyKey ( 'relabel' , 'sku-scan' , { relabelNo : this . current . relabelNo , ... payload } )
await this . run ( 'sku-scan' , async ( ) => {
const value = await scanRelabelSku ( this . current . relabelNo , { ... payload , idempotencyKey : key . value } )
this . applyCurrent ( value ) ; releaseWarehouseKey ( 'relabel' , key . signature )
const confirmed = value . items . find ( item => item . itemId === row . item . itemId ) ? . skuItems ? . [ row . index ]
this . lastSku = confirmed ? ` ${ confirmed . skuCode } 换标 ${ confirmed . newSkuCode } · ${ confirmed . quantity } 件 ` : '已保存,请重新读取标签绑定信息'
this . quickSku = '' ; this . skuChoices = [ ] ; this . activeTarget = 'quick-sku'
await new Promise ( resolve => uni . showModal ( { title : '请粘贴对应 SKU 标签' , content : ` ${ row . item . cartonMark } \ n ${ this . lastSku } ` , showCancel : false , confirmText : '知道了' , complete : resolve } ) )
} , true , key )
} ,
relabelTypeName ( type ) { return ( { CARTON _LABEL : '只换箱唛' , SKU _LABEL : '只换 SKU' , CARTON _AND _SKU : '混合换标(SKU + 箱唛)' } ) [ type ] || '换标类型待确认' } ,
async loadPhoto ( fileId ) {
const no = this . current ? . relabelNo
@@ -162,33 +203,54 @@
this . applyCurrent ( result )
releaseWarehouseKey ( 'relabel' , key . signature )
const confirmedItem = result . items . find ( ( row ) => row . itemId === item . itemId )
this . lastCarton = { cartonMark : mark , fbaCartonMark : confirmedItem ? . fbaCartonMark || item . fbaCartonMark }
this . lastCarton = { cartonMark : mark , fbaCartonMark : confirmedItem ? . fbaCartonMark || item . fbaCartonMark , destinationFbaWarehouseCode : confirmedItem ? . destinationFbaWarehouseCode || item . destinationFbaWarehouseCode }
this . quickCarton = ''
this . activeTarget = 'quick-carton'
this . successMessage = ` 本箱已处理,请贴 FBA 箱唛: ${ this . lastCarton . fbaCartonMark } `
await new Promise ( ( resolve ) => uni . showModal ( { title : '请粘贴对应箱唛' , content : ` 仓库唛头: ${ mark } \ n请贴 FBA 箱唛: ${ this . lastCarton . fbaCartonMark } ` , showCancel : false , confirmText : '知道了' , complete : resolve } ) )
this . successMessage = ''
await new Promise ( ( resolve ) => uni . showModal ( { title : '请粘贴对应箱唛' , content : ` 仓库唛头: ${ mark } \ n请贴 FBA 箱唛: ${ this . lastCarton . fbaCartonMark } \ nFBA 目的仓: ${ this . lastCarton . destinationFbaWarehouseCode || '待维护' } ` , showCancel : false , confirmText : '知道了' , complete : resolve } ) )
} , true , key )
} ,
removePhoto ( index ) { updateWarehouseWork ( 'relabel' , { photoFileIds : this . photoFileIds . filter ( ( _ , i ) => i !== index ) , photoNames : this . photoNames . filter ( ( _ , i ) => i !== index ) } ) } ,
startScanner ( ) { this . stopScanner ( ) ; this . _stopScanner = startScanBroadcast ( { onScan : ( value ) => { if ( this . busy ) return ; if ( this . activeTarget === 'quick-carton' ) { this . quickCarton = value ; this . submitCarton ( value ) } else this . handleHardwareScan ( value ) } , onError : ( error ) => this . setError ( error , '扫码广播注册失败' ) } ) } ,
startScanner ( ) { this . stopScanner ( ) ; this . _stopScanner = startScanBroadcast ( { onScan : ( value ) => { if ( this . busy ) return ; if ( this . activeTarget === 'pallet-work' ) { this . $refs . palletWork ? . handleScan ( value ) ; return } if ( this . activeTarget === 'quick-carton' ) { this . quickCarton = value ; this . submitCarton ( value ) } else this . handleHardwareScan ( value ) } , onError : ( error ) => this . setError ( error , '扫码广播注册失败' ) } ) } ,
stopScanner ( ) { if ( this . _stopScanner ) this . _stopScanner ( ) ; this . _stopScanner = null } ,
handleHardwareScan ( value ) { const skuTarget = /^sku-(old|new)-(\d+)$/ . exec ( this . activeTarget ) ; if ( skuTarget && this . selectedItem ) { const row = this . unitDraft . skuVerifications [ Number ( skuTarget [ 2 ] ) ] ; if ( row ) row [ skuTarget [ 1 ] === 'old' ? 'skuCode' : 'newSkuCode' ] = String ( value ) . trim ( ) ; if ( skuTarget [ 1 ] === 'old' ) this . activeTarget = ` sku-new- ${ skuTarget [ 2 ] } ` ; return } if ( this . activeTarget === 'old' ) { this . oldLabel = value ; this . activeTarget = this . taskTab === 'sku' ? 'sku-old-0' : this . action === 'RELABEL' ? 'new' : 'old' ; return } if ( this . activeTarget === 'new ' ) { this . newLabel = value ; return } this . taskCode = value ; this . openTask ( value ) } ,
handleOldLabel ( value ) { this . oldLabel = String ( value ? . detail ? . value ? ? value ? ? '' ) . trim ( ) ; if ( this . action === 'RELABEL' ) this . activeTarget = this . taskTab === 'sku' ? 'sku-old -0' : 'new' } ,
handleHardwareScan ( value ) { if ( this . activeTarget === 'quick-sku' ) { this . quickSku = value ; this . submitSku ( value ) ; return } if ( this . activeTarget === 'old ' ) { this . oldLabel = value ; return } this . taskCode = value ; this . openTask ( value ) } ,
handleOldLabel ( value ) { this . oldLabel = String ( value ? . detail ? . value ? ? value ? ? '' ) . trim ( ) ; if ( this . action === 'RELABEL' ) this . activeTarget = this . taskTab === 'sku' ? 'sku-new -0' : 'new' } ,
handleNewLabel ( value ) { this . newLabel = String ( value ? . detail ? . value ? ? value ? ? '' ) . trim ( ) } ,
async bootstrap ( ) { await this . loadCandidates ( ) ; if ( this . stage = == 'execute' && this . taskCode ) await this . openTask ( this . taskCode , false ) } ,
async bootstrap ( ) { await this . loadCandidates ( ) ; if ( this . error || this . stage ! == 'execute' ) return ; const owned = this . candidates . filter ( ( item ) => item . taskStatus === 'PROCESSING' && item . claimedByCurrentOperator === true ) ; const target = owned . find ( ( item ) => item . relabelNo === this . taskCode ) || ( owned . length === 1 ? owned [ 0 ] : null ) ; if ( target ) await this . openTask ( target . relabelNo , false ) } ,
async loadCandidates ( ) { if ( this . loading ) return ; this . loading = true ; this . error = null ; try { this . candidates = await listRelabelCandidates ( ) } catch ( error ) { this . setError ( error , '换标任务加载失败' ) } finally { this . loading = false } } ,
async openTask ( raw , sound = true ) { const no = String ( raw ? . detail ? . value ? ? raw ? ? this . taskCode ) . trim ( ) ; if ( ! no || this . busy ) return ; this . taskCode = no ; await this . run ( 'detail' , async ( ) => { this . applyCurrent ( await getRelabelTask ( no ) ) ; if ( sound ) playSuccessSound ( ) } , false ) } ,
applyCurrent ( value ) { const changed = this . current ? . relabelNo !== value . relabelNo ; this . current = value ; this . taskCode = value . relabelNo ; if ( changed ) { this . unitDrafts = { } ; this . documents = [ ] ; this . quickCarton = '' ; this . lastCarton = null ; if ( warehouseWork . relabel . photoTaskNo !== value . relabelNo ) updateWarehouseWork ( 'relabel' , { photoTaskNo : value . relabelNo , photoFileIds : [ ] , photoNames : [ ] , remark : '' } ) ; this . changeTab ( value . items . some ( ( item ) => item . operationType === 'SKU_LABEL' ) ? 'sku' : 'carton' ) ; this . loadDocuments ( ) } if ( this . selectedItem ) this . selectedItem = this . visibleItems . find ( ( item ) => item . itemId === this . selectedItem . itemId ) || null } ,
changeTab ( value ) { this . taskTab = value ; this . selectedItem = null ; this . reason = '' ; this . activeTarget = value === 'carton' ? 'quick-carton' : 'ta sk' } ,
changeTab ( value ) { this . taskTab = value ; this . selectedItem = null ; this . reason = '' ; this . quickSku = '' ; this . skuChoices = [ ] ; this . activeTarget = value === 'carton' ? 'quick-carton' : 'quick- sku ' } ,
skuDetails ( item ) { return item . skuItems ? . length ? item . skuItems : [ { skuCode : item . skuCode , newSkuCode : item . newSkuCode , quantity : null } ] } ,
selectItem ( item ) { if ( this . taskTab === 'carton' ) { this . activeTarget = 'quick-carton' ; return } if ( this . remaining ( item ) <= 0 ) return ; if ( ! this . unitDrafts [ item . itemId ] ) this . unitDrafts [ item . itemId ] = { cartonMark : '' , fbaCartonMark : '' , skuVerifications : this . skuDetails ( item ) . map ( ( sku ) => ( { skuCode : '' , newSkuCode : '' , quantity : sku . quantity } ) ) } ; this . selectedItem = item ; this . reason = '' ; this . activeTarget = 'sku- old-0 ' } ,
remaining ( item ) { const processed = this . taskTab === 'sku' ? item . skuProcessedPackages : item . cartonProcessedPackages ; return Math . max ( 0 , item . expectedPackages - Number ( processed ? ? item . relabelledPackages ) - item . skippedPackages - item . exceptionPackages ) } ,
selectItem ( item ) { if ( this . taskTab === 'carton' ) { this . activeTarget = 'quick-carton' ; return } if ( this . remaining ( item ) <= 0 ) return ; if ( ! this . unitDrafts [ item . itemId ] ) this . unitDrafts [ item . itemId ] = { cartonMark : '' , fbaCartonMark : '' , skuVerifications : this . skuDetails ( item ) . map ( ( sku ) => ( { skuCode : sku . skuCode || '' , newSkuCode : '' , quantity : sku . quantity } ) ) } ; this . selectedItem = item ; this . reason = '' ; this . activeTarget = 'old' } ,
focusNextSku ( index ) { const next = index + 1 ; this . activeTarget = next < this . unitDraft . skuVerifications . length ? ` sku-new- ${ next } ` : 'old' } ,
remaining ( item ) { const processed = this . taskTab === 'sku' ? item . skuProcessedPackages : item . cartonProcessedPackages ; const completed = item . operationType === 'CARTON_AND_SKU' ? Number ( processed || 0 ) : Number ( processed ? ? item . relabelledPackages ) ; return Math . max ( 0 , item . expectedPackages - completed - item . skippedPackages - item . exceptionPackages ) } ,
changeAction ( value ) { this . action = value ; this . newLabel = '' ; this . reason = '' ; this . activeTarget = 'old' } ,
async claimTask ( ) { if ( ! this . current ) return ; const payload = { relabelNo : this . current . relabelNo } ; const key = warehouseIdempotencyKey ( 'relabel' , 'claim' , payload ) ; await this . run ( 'claim' , async ( ) => { this . applyCurrent ( await claimRelabelTask ( this . current . relabelNo , { idempotencyKey : key . value } ) ) ; releaseWarehouseKey ( 'relabel' , key . signature ) ; this . successMessage = '换标任务已认领,可进入换标执行' ; await this . loadCandidates ( ) } , true , key ) } ,
async promptActiveTask ( active ) {
if ( this . busy ) return
this . busy = true
this . error = null
try {
const confirmed = await new Promise ( ( resolve ) => uni . showModal ( {
title : '已有正在执行的换标任务' ,
content : ` 当前账号正在执行 ${ active . relabelNo } ,请先完成该任务。是否前往继续作业? ` ,
confirmText : '前往任务' , cancelText : '取消' ,
success : ( result ) => resolve ( result . confirm ) , fail : ( ) => resolve ( false )
} ) )
if ( ! confirmed ) return
this . taskCode = active . relabelNo
uni . redirectTo ( { url : '/pages/relabel/relabel?op=relabelExecute' } )
} finally { this . busy = false }
} ,
async openExecution ( item ) {
if ( this . busy || this . stage ! == 'receive ' ) return
const active = this . candidates . find ( ( candidate ) => candidate . taskStatus === 'PROCESSING' && candidate . claimedByCurrentOperator )
if ( active && active . relabelNo !== item . relabelNo ) return this . setError ( new Error ( ` 当前账号正在执行 ${ active . relabelNo } ,请先完成后再操作其他换标单 ` ) )
if ( this . busy || this . stage = == 'review ' ) return
if ( this . stage === 'execute' ) {
if ( item . taskStatus !== 'PROCESSING' || item . claimedByCurrentOperator !== true ) return
return this . openTask ( item . relabelNo )
}
const active = this . candidates . find ( ( candidate ) => candidate . taskStatus === 'PROCESSING' && candidate . claimedByCurrentOperator === true )
if ( active && active . relabelNo !== item . relabelNo ) return this . promptActiveTask ( active )
this . taskCode = item . relabelNo
if ( item . taskStatus === 'PROCESSING' && item . claimedByCurrentOperator ) return uni . redirectTo ( { url : '/pages/relabel/relabel?op=relabelExecute' } )
const payload = { relabelNo : item . relabelNo } ; const key = warehouseIdempotencyKey ( 'relabel' , 'claim' , payload ) ; let claimed = false
@@ -196,7 +258,7 @@
if ( claimed ) uni . redirectTo ( { url : '/pages/relabel/relabel?op=relabelExecute' } )
} ,
async submitUnit ( ) { if ( ! this . canSubmitUnit || this . busy ) return ; const base = { itemId : this . selectedItem . itemId , oldLabel : this . oldLabel . trim ( ) , expectedVersion : this . current . version } ; const payload = { relabelNo : this . current . relabelNo , action : this . action , ... base , newLabel : this . newLabel . trim ( ) , skuVerifications : this . unitDraft . skuVerifications . map ( ( sku ) => ( { ... sku , skuCode : sku . skuCode . trim ( ) , newSkuCode : sku . newSkuCode . trim ( ) } ) ) , reason : this . reason . trim ( ) , exceptionCode : this . exceptionCode } ; const key = warehouseIdempotencyKey ( 'relabel' , 'unit' , payload ) ; await this . run ( 'unit' , async ( ) => { let value ; if ( this . action === 'SKIP' ) value = await skipRelabelUnit ( this . current . relabelNo , { ... base , reason : this . reason . trim ( ) , idempotencyKey : key . value } ) ; else if ( this . action === 'EXCEPTION' ) value = await reportRelabelException ( this . current . relabelNo , { ... base , exceptionCode : this . exceptionCode , reason : this . reason . trim ( ) , idempotencyKey : key . value } ) ; else value = await relabelUnit ( this . current . relabelNo , { ... base , newLabel : this . newLabel . trim ( ) , skuVerifications : payload . skuVerifications , idempotencyKey : key . value } ) ; releaseWarehouseKey ( 'relabel' , key . signature ) ; this . applyCurrent ( value ) ; this . oldLabel = '' ; this . newLabel = '' ; this . reason = '' ; this . activeTarget = 'old' ; this . successMessage = '本箱处理结果已保存' } , true , key ) } ,
choosePhoto ( ) { if ( this . busy || ! this . current || this . photoFileIds . length >= 20 ) return ; const no = this . current . relabelNo ; uni . chooseImage ( { count : Math . min ( 9 , 20 - this . photoFileIds . length ) , sourceType : [ 'camera' , 'album' ] , success : async ( { tempFilePaths , tempFiles } ) => { if ( this . current ? . relabelNo !== no ) return ; await this . run ( 'photo' , async ( ) => { for ( const [ index , path ] of ( tempFilePaths || [ ] ) . entries ( ) ) { if ( Number ( tempFiles ? . [ index ] ? . size || 0 ) > 10 * 1024 * 1024 ) throw new Error ( '照片不能超过 10MB' ) ; const file = await uploadRelabelPhoto ( no , path ) ; this . photoUrls [ String ( file . fileId ) ] = path ; updateWarehouseWork ( 'relabel' , { photoTaskNo : no , photoFileIds : [ ... this . photoFileIds , String ( file . fileId ) ] , photoNames : [ ... this . photoNames , file . fileName || '换标现场照片' ] } ) } this . successMessage = '现场照片已上传' } , false ) } } ) } ,
choosePhoto ( ) { if ( this . busy || ! this . current || this . photoFileIds . length >= 20 ) return ; const no = this . current . relabelNo ; uni . chooseImage ( { count : Math . min ( 9 , 20 - this . photoFileIds . length ) , sourceType : [ 'camera' , 'album' ] , success : async ( { tempFilePaths , tempFiles } ) => { if ( this . current ? . relabelNo !== no ) return ; await this . run ( 'photo' , async ( ) => { for ( const [ index , path ] of ( tempFilePaths || [ ] ) . entries ( ) ) { const selected = tempFiles ? . [ index ] ? . file || tempFiles ? . [ index ] ; if ( Number ( tempFiles ? . [ index ] ? . size || 0 ) > 10 * 1024 * 1024 ) throw new Error ( '照片不能超过 10MB' ) ; const uploadSource = typeof Blob !== 'undefined' && selected instanceof Blob ? selected : path ; const file = await uploadRelabelPhoto ( no , uploadSource ) ; this . photoUrls [ String ( file . fileId ) ] = path ; updateWarehouseWork ( 'relabel' , { photoTaskNo : no , photoFileIds : [ ... this . photoFileIds , String ( file . fileId ) ] , photoNames : [ ... this . photoNames , file . fileName || '换标现场照片' ] } ) } this . successMessage = '现场照片已上传' } , false ) } } ) } ,
async uploadPhoto ( path ) { await this . run ( 'photo' , async ( ) => { const file = await uploadRelabelPhoto ( this . current . relabelNo , path ) ; updateWarehouseWork ( 'relabel' , { photoFileIds : [ ... this . photoFileIds , String ( file . fileId ) ] , photoNames : [ ... this . photoNames , file . fileName || '换标现场照片' ] } ) ; this . successMessage = '现场照片已上传' } , false ) } ,
async completeTask ( ) { if ( this . busy || ! this . photoFileIds . length || ( ! this . allProcessed && ! this . remark . trim ( ) ) ) return ; const payload = { photoFileIds : [ ... this . photoFileIds ] , expectedVersion : this . current . version , remark : this . remark . trim ( ) , manualCompletion : ! this . allProcessed } ; const no = this . current . relabelNo ; if ( payload . manualCompletion ) { const confirmed = await new Promise ( ( resolve ) => uni . showModal ( { title : '手动完成换标' , content : '未处理箱将登记异常,关联尾程订单将标为异常。确认提交?' , success : ( result ) => resolve ( result . confirm ) , fail : ( ) => resolve ( false ) } ) ) ; if ( ! confirmed || this . current ? . relabelNo !== no || this . current . version !== payload . expectedVersion ) return } const key = warehouseIdempotencyKey ( 'relabel' , 'complete' , { relabelNo : no , ... payload } ) ; let completed = false ; await this . run ( 'complete' , async ( ) => { await completeRelabelTask ( no , { ... payload , idempotencyKey : key . value } ) ; releaseWarehouseKey ( 'relabel' , key . signature ) ; updateWarehouseWork ( 'relabel' , { currentNo : '' , photoFileIds : [ ] , photoNames : [ ] , remark : '' } ) ; completed = true } , true , key ) ; if ( completed ) this . goToClaimList ( ) } ,
goToClaimList ( ) { this . taskCode = '' ; uni . redirectTo ( { url : '/pages/relabel/relabel?op=relabelReceive' } ) } ,