@@ -2,36 +2,62 @@
< view v-if = "user" class="page-shell portal-page work-page" >
< PortalHeader :title = "title" :user = "user" fallback -url = " / pages / module / module ? key = relabel " / >
< scroll-view class = "work-scroll" scroll -y > < view class = "portal-content work-content" >
< view class = "work-heading" > < text class = "work-title" > { { title } } < / text > < text class = "work-subtitle" > 任务和仓库范围由服务端按当前 PDA Token 校验 ; 扫描与人工输入使用同一正式接口 < / text > < / view >
< view class = "work-card" > < ScanTargetInput name = "relabelTask" v-model = "taskCode" label="换标单号" placeholder="扫描或输入换标单号" :active="activeTar get === 'task'" @activate="activeTarget = 'task'" @submit="openTask" / ><button class="prim ary" :disabled="!taskCode.trim() || busy" @tap="openTask(taskCode)" > {{ busyAction = = = ' detail ' ? ' 查询中 … ' : ' 打开换标任务 ' }} < / button > < / view >
< view class = "work-heading" > < text class = "work-title" > { { title } } < / text > < text class = "work-subtitle" > { { stageSubtitle } } < / text > < / view >
< view v-if = "sta ge === 'execute'" class="work-card" ><button class="second ary" :disabled="busy" @tap="goToClaimList" > 返回换标列表 < / button > < / view >
< view class = "row-head" > < text class = "card-title" > { { candidateHeading } } < / text > < button class = "link-button" :disabled = "loading" @tap ="loadCandidates" > {{ loading ? ' 刷新中 … ' : ' 刷新 ' }} < / 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 >
< 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 class = "task-list" > < view v-for = "item in visibleCandidates" :key="item.relabelNo" class="task-card" :class="{ current: current?.relabelNo === item.relabelNo }" @tap="openTask(item.relabelNo )"><view class="row-head"><view><text class="business-code">{{ item.relabelNo }}</text><text class="meta-line">客户订单:{{ item.customerOrderNo || '—' }}</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-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 >
< view v-if = "c urrent" class="work-card" >
< view v-if = "showC urrent" 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 >
< view class = "metric-row" > < view class = "metric" > < strong > { { current . expectedPackages } } < / strong > < text > 应处理 < / text > < / view > < view class = "metric" > < strong > { { current . relabelledPackages } } < / strong > < text > 已换标 < / text > < / view > < view class = "metric" > < strong > { { current . skippedPackages } } < / strong > < text > 跳过 < / text > < / view > < view class = "metric" > < strong > { { current . exceptionPackages } } < / strong > < text > 异常 < / text > < / view > < / view >
< template v-if = "stage === 'receive'"><text class="hint">认领后任务进入处理中,并锁定当前操作人;其他账号不可并发作业。</text><button class="primary" :disabled="busy || current.taskStatus === 'WAIT_REVIEW' || current.taskStatus === 'COMPLETED'" @tap="claimTask" > {{ busyAction = = = ' claim ' ? ' 认领中 … ' : current.claimedByCurrentOperator ? ' 恢复当前换标任务 ' : ' 确认收货并认领任务 ' }} < / button > < / template >
< template v-else-if = "stage === 'execute'" >
< view v-if = "current.taskStatus !== 'PROCESSING'" class="empty-box section-gap"><text class="empty-copy" > 当前任务不是处理中状态 , 请先在 “ 换标收货 ” 认领 , 或等待复核退回 。 < / text > < / view >
< view class = "section-gap" > < view class = "row-head" > < text class = "card-title" > 标签文件 < / text > < button class = "link-button" :disabled = "busy" @tap ="loadDocuments" > 刷新文件 < / button > < / view > < text v-if = "!documents.length" class="meta-line">暂无标签文件</text><view v-for="file in documents" :key="`${file.scope}:${file.fileId}`" class="row-head"><text class="meta-line">{{ file.scope === 'CARTON_TOTAL' ? '总箱唛' : file.scope === 'SKU_TOTAL' ? '总 SKU 标签' : '明细标签' }}: {{ file.fileName }}</text><button class="secondary" :disabled="busy" @tap="printDocument(file)" > 查看 / 打印 < / button > < / view > < / view >
< template v-if = "stage === 'execute'" >
< view v-if = "current.reviewDecision === 'REWORK' && current.reviewRemark" class="error-box section-gap"><text class="error-title" > 退回重新贴标 < / text > < text > { { current . reviewRemark } } < / text > < / view >
< view v-if = "current.taskStatus !== 'PROCESSING'" class="empty-box section-gap"><text class="empty-copy" > 当前任务不是处理中状态 , 请返回换标列表选择可执行任务 , 或等待复核退回 。 < / text > < / view >
< template v-else >
< text class = "card-title section-gap " > 选择商品明细 < / text >
< view class = "item-list" > < view v-for = "item in current.items" :key="item.itemId" class="item-card" :class="{ done: remaining(item) === 0, current: selectedItem?.itemId === item.itemId }" @tap="selectItem(item)"><view class="row-head"><view><text class="business-code">{{ item.skuCode || 'SKU 未关联' }}</text><text class="meta-line">{{ item.productNameZh || '商品名称未关联' }}</text><text class="meta-line">新 SKU: {{ item.newSkuCode || '—' }}</text></view><text class="status-pill" :class="{ good: remaining(item) === 0 }">待处理 {{ remaining(item) }}</text></view><text class="meta-line" > {{ item.labelRequirement | | ' 无额外标签要求 ' }} < / text > < / view > < / view >
< template v-if = "selectedItem && remaining(selectedItem) > 0" >
< 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" 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 || '—' } } < / 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 > < / view >
< template v-if = "taskTab === 'sku' && selectedItem && 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" / >
< ScanTargetInput v-if = "action === 'RELABEL'" name="newLabel" v-model="newLabel" label="新标签" :placeholder="`应匹配 ${selectedItem.newSkuCode || '审批新 SKU'}`" :active="activeTarget === 'new'" @activate="activeTarget = 'new'" @submit="handleNewLabel" / >
< 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 = "work-card section-gap" > < view class = "row-head" > < text class = "card-title" > 完成照片 < / text > < text class = "status-pill" > { { photoFileIds . length } } 张 < / text > < / view > < button class = "secondary" :disabled = "busy" @tap ="choosePhoto" > 拍照或从相册选择 < / button > < text v-for = "(name,index) in photoNames" :key="`${name}:${index}`" class="meta-line">{{ index + 1 }}. {{ name || '换标现场照片' }}</text><textarea v-model="remark" class="reason-input section-gap" maxlength="1000" placeholder="完成备注(选填)" /><button class="primary" :disabled="busy || !allProcessed || !photoFileIds.length" @tap="completeTask">{{ busyAction === 'complete' ? '提交中…' : '完成现场换标并等待复核' }}</button><text class="hint" > 每件货物必须换标 、 跳过或登记异常 , 且至少上传一张现场照片 。 < / text > < / view >
< view class = "section-gap" >
< 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" >
< image v-if = "photoUrls[fileId]" class="photo-thumbnail" :src="photoUrls[fileId]" mode="aspectFill" @tap="previewPhoto(fileId)" / >
< button v-else class = "photo-thumbnail photo-placeholder" @tap ="loadPhoto(fileId)" > {{ photoErrors [ fileId ] ? ' 加载失败 , 点击重试 ' : ' 照片加载中 ' }} < / button >
< text class = "photo-name" > { { photoNames [ index ] || '换标现场照片' } } < / text >
< 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 >
< / template >
< / template >
< template v-else > < view v-if = "current.taskStatus !== 'WAIT_REVIEW'" class="empty-box section-gap"><text class="empty-copy">当前任务尚未进入待复核状态。</text></view><template v-else><textarea v-model="remark" class="reason-input section-gap" maxlength="1000" placeholder="复核备注;退回重做时建议填写原因" /><view class="action-row"><button class="danger" :disabled="busy" @tap="review('REWORK')">退回重做</button><button class="primary" :disabled="busy" @tap="review('APPROVE')" > 复核通过 < / button > < / view > < / template > < / template >
< / 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 class="secondary" @tap="retry" > 重新读取任务 < / button > < / view >
@@ -47,50 +73,133 @@
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 { 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 } ,
data ( ) { return { user : session . currentUser , stage : 'receive' , 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 : '其他' } ] } } ,
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 : {
title ( ) { return ( { receive : '换标收货 ', execute : '换标执行' , review : '换标复核' } ) [ this . stage ] } ,
candidateHeading ( ) { return this . stage === 'review' ? '待复核任务 ' : this . stage === 'execute' ? '处理中任务' : '可认领任务 ' } ,
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 ) } ,
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 : [ ] } } ,
skuVerified ( ) { if ( ! this . selectedItem || this . selectedItem . operationType === 'CARTON_LABEL' ) return true ; const expected = this . skuDetails ( this . selectedItem ) ; return expected . length > 0 && expected . every ( ( sku , index ) => { const actual = this . unitDraft . skuVerifications [ index ] ; return actual && actual . skuCode . trim ( ) === sku . skuCode && actual . newSkuCode . trim ( ) === sku . newSkuCode && Number ( actual . quantity ) > 0 && Number ( actual . quantity ) === Number ( sku . quantity ) } ) } ,
cartonVerified ( ) { return Boolean ( this . selectedItem && this . oldLabel . trim ( ) === this . selectedItem . cartonMark && ( this . taskTab === 'sku' || this . newLabel . trim ( ) === this . selectedItem . fbaCartonMark && this . newLabel . trim ( ) ) ) } ,
title ( ) { return ( { receive : '换标列表' , execute : '换标执行' , review : '换标复核' } ) [ this . stage ] } ,
candidateHeading ( ) { return this . stage === 'review' ? '待复核任务' : this . stage === 'execute' ? '处理中任务' : '换标任务' } ,
emptyText ( ) { return ` 暂无 ${ this . candidateHeading } ` } ,
visibleCandidates ( ) { if ( this . stage === 'review' ) return this . candidates . filter ( ( item ) => item . taskStatus === 'WAIT_REVIEW' ) ; if ( this . stage === 'execute' ) return this . candidates . filter ( ( item ) => item . taskStatus === 'PROCESSING' && item . claimedByCurrentOperator ) ; return this . candidates . filter ( ( item ) => ! item . taskStatus || ( item . taskStatus === 'PROCESSING' && item . claimedByCurrentOperator ) ) } ,
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 ) ) } ,
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 warehouseWork . relabel . oldLabel } , set ( value ) { updateWarehouseWork ( 'relabel' , { oldLabel : value } ) } } ,
newLabel : { get ( ) { return warehouseWork . relabel . newLabel } , set ( value ) { updateWarehouseWork ( 'relabel' , { newLabel : value } ) } } ,
oldLabel : { get ( ) { return this . unitDraft . cartonMark } , set ( value ) { if ( this . selectedItem ) this . unitDraft . cartonMark = value } } ,
newLabel : { get ( ) { return this . unitDraft . fbaCartonMark } , set ( value ) { if ( this . selectedItem ) this . unitDraft . fbaCartonMark = value } } ,
action : { get ( ) { return warehouseWork . relabel . action } , set ( value ) { updateWarehouseWork ( 'relabel' , { action : value } ) } } ,
reason : { get ( ) { return warehouseWork . relabel . reason } , set ( value ) { updateWarehouseWork ( 'relabel' , { reason : value } ) } } ,
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 } ,
canSubmitUnit ( ) { if ( ! this . selectedItem || ! this . oldLabel . trim ( ) ) return false ; if ( this . action === 'RELABEL' ) return Boolean ( this . newLabel . trim ( ) ) ; return Boolean ( this . reason . trim ( ) ) } ,
unitActionText ( ) { return ( { RELABEL : '确认本件 换标' , SKIP : '确认跳过本件 ' , EXCEPTION : '登记本件 异常' } ) [ this . action ] }
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' } ,
onShow ( ) { if ( ! requireSession ( ) ) return ; this . user = session . currentUser ; restoreWarehouseWork ( ` ${ this . user . id } : ${ this . user . warehouseId } ` ) ; this . startScanner ( ) ; this . bootstrap ( ) } ,
mixins : [ { data ( ) { return { documents : [ ] , quickCarton : '' , lastCarton : null , 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 ) } } ,
'current.relabelNo' ( ) { this . photoUrls = { } ; this . photoErrors = { } ; for ( const id of this . photoFileIds ) this . loadPhoto ( id ) }
} ,
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 : {
startScanner ( ) { this . stopScanner ( ) ; this . _stopScanner = startScanBroadcast ( { onScan : ( value ) => this . handleHardwareScan ( value ) , onError : ( error ) => this . setError ( error , '扫码广播注册失败' ) } ) } ,
relabelTypeName ( type ) { return ( { CARTON _LABEL : '只换箱唛' , SKU _LABEL : '只换 SKU' , CARTON _AND _SKU : '混合换标(SKU + 箱唛)' } ) [ type ] || '换标类型待确认' } ,
async loadPhoto ( fileId ) {
const no = this . current ? . relabelNo
if ( ! no || this . photoUrls [ fileId ] || ! this . photoFileIds . includes ( fileId ) ) return
this . photoErrors [ fileId ] = false
try { const path = await downloadRelabelPhoto ( no , fileId ) ; if ( this . current ? . relabelNo === no && this . photoFileIds . includes ( fileId ) ) this . photoUrls [ fileId ] = path }
catch ( _ ) { if ( this . current ? . relabelNo === no ) this . photoErrors [ fileId ] = true }
} ,
previewPhoto ( fileId ) { const urls = this . photoFileIds . map ( ( id ) => this . photoUrls [ id ] ) . filter ( Boolean ) ; if ( this . photoUrls [ fileId ] ) uni . previewImage ( { current : this . photoUrls [ fileId ] , urls } ) } ,
retakePhoto ( index ) {
if ( this . busy || ! this . current ) return
const no = this . current . relabelNo ; const oldId = this . photoFileIds [ index ]
uni . chooseImage ( { count : 1 , sourceType : [ 'camera' ] , success : ( { tempFilePaths , tempFiles } ) => {
if ( this . current ? . relabelNo !== no || ! this . photoFileIds . includes ( oldId ) || ! tempFilePaths ? . [ 0 ] ) return
this . run ( 'photo' , async ( ) => {
if ( Number ( tempFiles ? . [ 0 ] ? . size || 0 ) > 10 * 1024 * 1024 ) throw new Error ( '照片不能超过 10MB' )
const file = await uploadRelabelPhoto ( no , tempFilePaths [ 0 ] )
const ids = [ ... this . photoFileIds ] ; const names = [ ... this . photoNames ] ; const position = ids . indexOf ( oldId )
if ( position < 0 || this . current ? . relabelNo !== no ) return
ids [ position ] = String ( file . fileId ) ; names [ position ] = file . fileName || '换标现场照片'
this . photoUrls [ String ( file . fileId ) ] = tempFilePaths [ 0 ]
updateWarehouseWork ( 'relabel' , { photoFileIds : ids , photoNames : names } )
this . successMessage = '照片已重新拍摄并替换'
} , false )
} } )
} ,
async loadDocuments ( ) { if ( ! this . current ) return ; const no = this . current . relabelNo ; try { const files = await listRelabelDocuments ( no ) ; if ( this . current ? . relabelNo === no ) this . documents = files || [ ] } catch ( error ) { this . setError ( error , '标签文件加载失败' ) } } ,
async printDocument ( file ) { await this . run ( 'document' , async ( ) => { const path = await downloadRelabelDocument ( this . current . relabelNo , file ) ;
// #ifdef H5
const frame = document . createElement ( 'iframe' ) ; frame . style . cssText = 'position:fixed;width:1px;height:1px;border:0' ; frame . src = path ; frame . onload = ( ) => { try { frame . contentWindow . focus ( ) ; frame . contentWindow . print ( ) } catch ( _ ) { window . open ( path , '_blank' , 'noopener' ) } } ; document . body . appendChild ( frame ) ; setTimeout ( ( ) => frame . remove ( ) , 120000 ) ;
// #endif
// #ifndef H5
await new Promise ( ( resolve , reject ) => uni . openDocument ( { filePath : path , fileType : 'pdf' , showMenu : true , success : resolve , fail : reject } ) ) ;
// #endif
} , false ) } ,
async submitCarton ( raw ) {
const mark = String ( raw ? . detail ? . value ? ? raw ? ? this . quickCarton ) . trim ( )
if ( ! mark || this . busy || ! this . current ) return
const item = this . current . items . find ( ( row ) => row . cartonMark === mark && row . operationType !== 'SKU_LABEL' )
if ( ! item ) return this . setError ( new Error ( '该仓库唛头不属于本单箱唛换标明细' ) )
const payload = { relabelNo : this . current . relabelNo , cartonMark : mark , expectedVersion : this . current . version }
const key = warehouseIdempotencyKey ( 'relabel' , 'carton-scan' , payload )
await this . run ( 'carton-scan' , async ( ) => {
const result = await scanRelabelCarton ( payload . relabelNo , { cartonMark : mark , expectedVersion : payload . expectedVersion , idempotencyKey : key . value } )
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 . 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 } ) )
} , 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 , '扫码广播注册失败' ) } ) } ,
stopScanner ( ) { if ( this . _stopScanner ) this . _stopScanner ( ) ; this . _stopScanner = null } ,
handleHardwareScan ( value ) { if ( this . activeTarget === 'old' ) { this . oldLabel = value ; this . activeTarget = 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 = 'new' } ,
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' } ,
handleNewLabel ( value ) { this . newLabel = String ( value ? . detail ? . value ? ? value ? ? '' ) . trim ( ) } ,
async bootstrap ( ) { await this . loadCandidates ( ) ; if ( this . taskCode ) await this . openTask ( this . taskCode , false ) } ,
async bootstrap ( ) { await this . loadCandidates ( ) ; if ( this . stage === 'execute' && this . taskCode ) await this . openTask ( this . taskCode , 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 ) { this . current = value ; this . taskCode = value . relabelNo ; if ( this . selectedItem ) this . selectedItem = value . items . find ( ( item ) => item . itemId === this . selectedItem . itemId ) || null } ,
selectItem ( item ) { if ( this . remaining ( item ) < = 0 ) return ; this . selectedItem = item ; this . oldLabel = '' ; this . newLabel = '' ; this . reason = '' ; this . activeTarget = 'old ' } ,
remaining ( item ) { return Math . max ( 0 , item . expectedPackages - item . relabelledPackages - item . skippedPackages - item . exceptionPackages ) } ,
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 ' : 'task ' } ,
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 ) } ,
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 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 ( ) , 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 ( ) , 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 ) return ; uni . chooseImage ( { count : 1 , sourceType : [ 'camera' , 'album' ] , success : ( { tempFilePaths , tempFiles } ) => { const path = tempFilePaths ? . [ 0 ] ; if ( Number ( tempFiles ? . [ 0 ] ? . size || 0 ) > 10 * 1024 * 1024 ) return this . setError ( new Error ( '照片不能超过 10MB' ) ) ; if ( path ) this . uploadPhoto ( path ) } } ) } ,
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 } ,请先完成后再操作其他换标单 ` ) )
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
await this . run ( 'claim' , async ( ) => { this . applyCurrent ( await claimRelabelTask ( item . relabelNo , { idempotencyKey : key . value } ) ) ; releaseWarehouseKey ( 'relabel' , key . signature ) ; claimed = true } , true , key )
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 ) } } ) } ,
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 . allProcessed || ! this . photoFileIds . length ) return ; const payload = { relabelNo : this . current . relabelNo , photoFileIds : this . photoFileIds , expectedVersion : this . current . version , remark : this . remark . trim ( ) } ; const key = warehouseIdempotencyKey ( 'relabel' , 'complete' , payload ) ; await this . run ( 'complete' , async ( ) => { this . applyCurrent ( await completeRelabelTask ( this . current . relabelNo , { photoFileIds : this . photoFileIds , expectedVersion : this . current . version , idempotencyKey : key . value , remark : this . remark . trim ( ) || undefined } ) ) ; releaseWarehouseKey ( 'relabel' , key . signature ) ; updateWarehouseWork ( 'relabel' , { photoFileIds : [ ] , photoNames : [ ] , remark : '' } ) ; this . successMessage = '现场换标已提交复核' ; await this . loadCandidates ( ) } , true , key ) } ,
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' } ) } ,
async review ( decision ) { if ( ! this . current || this . current . taskStatus !== 'WAIT_REVIEW' ) return ; const payload = { relabelNo : this . current . relabelNo , decision , expectedVersion : this . current . version , remark : this . remark . trim ( ) } ; const key = warehouseIdempotencyKey ( 'relabel' , 'review' , payload ) ; await this . run ( 'review' , async ( ) => { this . applyCurrent ( await reviewRelabelTask ( this . current . relabelNo , { decision , expectedVersion : this . current . version , idempotencyKey : key . value , remark : this . remark . trim ( ) || undefined } ) ) ; releaseWarehouseKey ( 'relabel' , key . signature ) ; this . remark = '' ; this . successMessage = decision === 'APPROVE' ? '换标复核已通过' : '换标任务已退回重做' ; await this . loadCandidates ( ) } , true , key ) } ,
async retry ( ) { if ( this . current ? . relabelNo ) await this . openTask ( this . current . relabelNo , false ) ; else await this . loadCandidates ( ) } ,
async reloadCurrent ( ) { if ( this . current ? . relabelNo ) this . applyCurrent ( await getRelabelTask ( this . current . relabelNo ) ) } ,
@@ -102,4 +211,6 @@
}
< / script >
< style scoped lang = "scss" > @ import '../warehouse/operations.scss' ; . action - row > . danger , . action - row > . primary { width : 50 % } . item - card . current { border - color : # 6 ea3ee ; background : # f3f8ff } < / style >
< style scoped lang = "scss" > @ import '../warehouse/operations.scss' ; . action - row > . danger , . action - row > . primary { width : 50 % } . item - card . current { border - color : # 6 ea3ee ; background : # f3f8ff } . tab - row { display : flex ; gap : 10 px ; margin : 14 px 0 10 px } . tab - button { flex : 1 ; border : 1 px solid # d7e1ef ; border - radius : 10 px ; background : # fff ; color : # 52647 c ; font - size : 15 px ; padding : 10 px 8 px } . tab - button . active { border - color : # 2167 e8 ; background : # 2167 e8 ; color : # fff ; font - weight : 600 }
. photo - grid { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; gap : 12 px ; margin - top : 12 px } . photo - tile { min - width : 0 ; overflow : hidden ; border : 1 px solid # d7e1ef ; border - radius : 8 px } . photo - thumbnail { display : block ; width : 100 % ; height : 140 px ; background : # f1f4f8 } . photo - placeholder { margin : 0 ; border : 0 ; border - radius : 0 ; font - size : 13 px ; white - space : normal ; line - height : 1.5 ; display : flex ; align - items : center ; justify - content : center } . photo - name { display : block ; padding : 6 px 8 px ; overflow : hidden ; white - space : nowrap ; text - overflow : ellipsis ; font - size : 12 px ; color : # 52647 c } . photo - actions { display : flex ; justify - content : space - around ; padding : 4 px } . photo - actions . link - button { margin : 0 }
< / style >