2026-09-11 11:15:07 +08:00
< template >
< view v-if = "user" class="page-shell palletization-page" >
2026-09-11 11:51:35 +08:00
< PortalHeader title = "一件代发入库" :user = "user" :fallback-url = "'/pages/dropship-tasks/dropship-tasks'" />
2026-09-11 11:15:07 +08:00
< view class = "content" >
< view v-if = "error" class="message error" role="alert" >{{ error }}</ view >
< view v-if = "notice" class="message success" role="status" >{{ notice }}</ view >
2026-09-11 11:51:35 +08:00
< view v-if = "loading && !task" class="state" > 正在加载入库任务 … </ view >
2026-09-11 11:15:07 +08:00
< template v-if = "task" >
2026-09-11 11:51:35 +08:00
< view class = "task-summary-card" >
2026-09-11 14:18:53 +08:00
< view class = "summary-head" >< text class = "inbound-code" >{{ task . inboundCode || '—' }}</ text >< text class = "status" >{{ statusLabel }}</ text ></ view >
2026-09-11 14:21:46 +08:00
< text class = "warehouse-summary" >{{ task . warehouseName || '仓库未关联' }}</ text >
2026-09-11 14:18:53 +08:00
< view class = "compact-metrics" >
< text >< strong >{{ task . totalCartons || 0 }}</ strong > 总箱 </ text >
< text >< strong >{{ task . countedCartons || 0 }}</ strong > 清点 </ text >
< text >< strong >{{ task . boundCartons || 0 }}</ strong > 归托 </ text >
< text >< strong >{{ task . unboundCartons || 0 }}</ strong > 待归 </ text >
< text class = "pallet-readiness" : class = "{ invalid: !palletSetupReady }" >< strong >{{ task . generatedPalletCount || 0 }} / {{ task . expectedPalletCount || 0 }}</ strong > 托盘 </ text >
2026-09-11 13:54:06 +08:00
</ view >
< view v-if = "!palletSetupReady" class="message error pallet-setup-error" role="alert" >{{ palletSetupMessage }}</ view >
2026-09-11 11:51:35 +08:00
< button v-if = "task.taskStatus === 'PENDING'" class="primary-button" :disabled="submitting" @tap="claimTask" > 领取并开始入库 </ button >
2026-09-11 11:15:07 +08:00
</ view >
2026-10-19 16:40:51 +08:00
< view v-if = "task.taskStatus === 'CLAIMED' || currentPallet" class="section-card" >
2026-09-11 14:15:05 +08:00
< text class = "section-title" > 1. 扫箱清点 </ text >
2026-09-11 13:54:06 +08:00
< template v-if = "palletSetupReady" >
2026-09-11 11:15:07 +08:00
< ScanTargetInput v-model = "boxCode" name="boxCode" label="箱码" placeholder="扫描或输入箱唛箱码" :active="activeScan === 'boxCode'" @activate="activeScan = 'boxCode'" @submit="resolveCarton" />
2026-09-11 13:54:06 +08:00
< view v-if = "!carton" class="count-entry" >
2026-09-11 14:15:05 +08:00
< text > 请扫描箱码 </ text >
2026-09-11 14:18:53 +08:00
< button class = "compact-action" :disabled = "!canResolveCarton" @tap ="resolveCarton()" > 读取 SKU </ button >
2026-09-11 13:54:06 +08:00
</ view >
2026-09-11 11:15:07 +08:00
< view v-if = "carton" class="carton-card" >
2026-10-19 16:40:51 +08:00
< text class = "carton-owner" >{{ scanTask ? . customerName || task . customerName }} · {{ carton . inboundCode }}</ text >
< text v-if = "carton.blockReason" class="pallet-hint blocked" >{{ carton.blockReason }}</ text >
2026-09-11 15:38:35 +08:00
< view v-for = "(line, index) in skuCounts" :key="line.inboundDetailId" class="sku-count-row" >
< view class = "sku-copy" >< text class = "sku-label" > SKU {{ index + 1 }}</ text >< text class = "sku-code" >{{ line . skuCode || '—' }}</ text ></ view >
2026-09-15 11:54:24 +08:00
< view v-if = "!isResolvedPendingCarton" class="quantity-stepper" >
2026-09-11 15:38:35 +08:00
< button : aria -label ="` 减少 $ { line.skuCode } 清点数量 `" @tap ="adjustQuantity(index, -1)" > − </ button >
< input v-model = "line.actualQuantity" type="number" :aria-label="`${line.skuCode} 清点数量`" />
< button : aria -label ="` 增加 $ { line.skuCode } 清点数量 `" @tap ="adjustQuantity(index, 1)" > + </ button >
2026-09-11 14:15:05 +08:00
</ view >
2026-09-15 11:54:24 +08:00
< text v-else class = "counted-quantity" > 已清点 {{ line . actualQuantity }} 件 </ text >
< view v-if = "!isResolvedPendingCarton && lineDiffers(line)" class="field stacked difference-field"><text>差异说明</text><textarea v-model="line.discrepancyRemark" maxlength="500" placeholder="数量不一致时必填" /></ view >
2026-09-11 14:15:05 +08:00
</ view >
2026-09-15 20:51:06 +08:00
< text v-if = "assignableCurrentPallet" class="pallet-hint" > 归入 {{ assignableCurrentPallet.palletNo }}</ text >
< text v-else-if = "currentPallet?.status === 'SEALED'" class="pallet-hint blocked" > 当前托盘已封托 , 请先解除封托或扫描其他托盘 </ text >
2026-09-15 11:54:24 +08:00
< text v-else-if = "isResolvedPendingCarton" class="pallet-hint" > 本箱已清点 , 请先扫描目标托盘 </ text >
< button class = "primary-button" : disabled = "!canSubmitCarton || submitting" @tap ="confirmCarton" >{{ cartonActionLabel }}</ button >
2026-09-11 11:15:07 +08:00
</ view >
2026-09-11 13:54:06 +08:00
< view class = "pallet-scan-block" >
2026-09-11 14:18:53 +08:00
< text class = "field-label" > 扫托盘 < text > 可选 </ text ></ text >
2026-09-11 13:54:06 +08:00
< ScanTargetInput v-model = "palletCode" name="palletCode" label="托盘码" placeholder="扫描或输入已生成的 DP 托盘码" :active="activeScan === 'palletCode'" @activate="activeScan = 'palletCode'" @submit="selectPallet" />
</ view >
</ template >
< view v-else class = "count-unavailable" > 托盘码未准备完成 , 当前不能开始箱码清点 。 </ view >
2026-09-11 11:15:07 +08:00
</ view >
2026-09-11 17:12:08 +08:00
< view v-if = "task.taskStatus === 'CLAIMED' && task.totalCartons > 0 && task.boundCartons === task.totalCartons" class="continue-strip" >
< text > 本票已全部归托 。 托盘未装满时先继续装其他票 , 实物装满后再封托 。 </ text >
2026-10-19 16:40:51 +08:00
< button class = "secondary-button" : disabled = "submitting || loading" @tap ="continueOtherTask" > 继续装其他入仓单 </ button >
2026-09-11 17:12:08 +08:00
</ view >
2026-09-11 11:15:07 +08:00
< view class = "section-card" >
2026-10-22 23:06:04 +08:00
< view class = "section-heading" >< text class = "section-title" > 2. 托盘操作 </ text >< button class = "refresh-button" :disabled = "loading" @tap ="loadTask" >{{ loading ? ' 刷新中 …' : ' 刷新 ' }}</ button ></ view >
2026-09-11 17:34:53 +08:00
< view v-if = "currentPallet" class="pallet-card" >
< view class = "pallet-head" >< text class = "pallet-code" >{{ currentPallet . palletNo }}</ text >< text class = "pallet-status" : class = "currentPallet.status === 'SEALED' ? 'sealed' : ''" >{{ currentPallet . status === 'SEALED' ? '已封托' : '当前托盘' }}</ text ></ view >
< text class = "pallet-meta" >{{ currentPallet . cartonCount || 0 }} 箱 · {{ currentPallet . skuCount || 0 }} SKU · {{ currentPallet . weightKg || 0 }} kg · {{ currentPallet . volumeCbm || 0 }} m³ </ text >
2026-10-19 16:40:51 +08:00
< text class = "pallet-meta" > 整托 {{ palletCustomerCount }} 个客户 · {{ palletMembers . length }} 票 </ text >
< view v-for = "member in palletMembers" :key="member.taskId" class="member-row" >< text >{{ member . customerName || '客户未关联' }} · {{ member . inboundCode }}</ text >< strong >{{ member . cartonCount }} 箱 </ strong ></ view >
< view v-for = "item in (currentPalletDetails?.cartons || [])" :key="item.cartonId" class="carton-row"><view><text>{{ item.boxCode }}</text><text class="carton-owner">{{ memberForCarton(item)?.customerName }} · {{ item.inboundCode }}</text></view><button v-if="currentPallet.status === 'OPEN'" class="text-button" :disabled="submitting || loading" @tap="moveCarton(item, null)" > 移回待绑托 </ button ></ view >
2026-09-11 17:34:53 +08:00
< button v-if = "currentPallet.palletId && currentPallet.status === 'OPEN'" class="primary-button" :disabled="!currentPallet.cartonCount || submitting" @tap="sealPallet(currentPallet)" > 封托 </ button >
2026-09-15 20:51:06 +08:00
< button v-if = "currentPallet.palletId && currentPallet.status === 'SEALED'" class="secondary-button unseal-button" :disabled="submitting" @tap="openUnseal(currentPallet)" > 解除封托 </ button >
2026-09-11 11:15:07 +08:00
</ view >
</ view >
</ template >
</ view >
2026-09-15 20:51:06 +08:00
< uni-popup ref = "unsealPopup" type = "center" :is-mask-click = "true" @change ="handleUnsealPopupChange" >
< view class = "unseal-dialog" role = "dialog" aria -modal =" true " aria -label =" 确认解除封托 " >
< PopupHeader title = "确认解除封托?" tone = "danger" @close ="closeUnseal" />
< text class = "unseal-copy" > 托盘 {{ unsealTarget ? . palletNo || '—' }} 将恢复为开放状态 , 可继续移箱 、 归托和重新封托 。 </ text >
< view class = "unseal-actions" >< button class = "secondary-button" @tap ="closeUnseal" > 取消 </ button >< button class = "danger-button" :disabled = "submitting" @tap ="confirmUnseal" > 确认解除 </ button ></ view >
</ view >
</ uni -popup >
2026-09-11 11:15:07 +08:00
</ view >
</ template >
< script >
import PortalHeader from '@/components/PortalHeader.vue'
2026-09-15 20:51:06 +08:00
import PopupHeader from '@/components/PopupHeader.vue'
2026-09-11 11:15:07 +08:00
import ScanTargetInput from '@/components/ScanTargetInput.vue'
2026-10-19 16:40:51 +08:00
import { bindDropshipPendingCartons , claimDropshipPalletizationTask , countDropshipCarton , getDropshipPalletizationTask , moveDropshipCarton , resolveDropshipWorkspaceCarton , resolveDropshipWorkspacePallet , sealDropshipPallet , unsealDropshipPallet } from '@/api/pda-dropship.js'
2026-09-11 11:15:07 +08:00
import { session , requireSession } from '@/stores/session.js'
2026-10-23 18:45:48 +08:00
import { sanitizeUserMessage } from '@/utils/user-facing.js'
2026-09-14 11:05:48 +08:00
import { stableOperationId } from '@/utils/idempotency.js'
import { isOnline } from '@/utils/network.js'
import { clearDropshipPalletizingDraft , restoreDropshipPalletizingDraft , saveDropshipPalletizingDraft } from '@/stores/dropship-palletizing.js'
import { createDropshipScanState , planDropshipScan , DROPSHIP_SCAN_INTENT } from '@/utils/dropship-palletizing-state.js'
2026-09-11 11:15:07 +08:00
import { startScanBroadcast } from '@/utils/scan-broadcast.js'
export default {
2026-09-15 20:51:06 +08:00
components : { PopupHeader , PortalHeader , ScanTargetInput },
2026-10-19 16:40:51 +08:00
data () { return { user : session . currentUser , taskId : '' , warehouseId : '' , task : null , scanTask : null , workspace : null , initialPalletNo : '' , palletCode : '' , selectedPalletId : '' , selectedPalletNo : '' , boxCode : '' , carton : null , skuCounts : [], activeScan : 'boxCode' , loading : false , submitting : false , error : '' , notice : '' , unsealOpen : false , unsealTarget : null } },
2026-09-11 11:15:07 +08:00
computed : {
statusLabel () { return this . task ? . taskStatus === 'COMPLETED' ? '已完成' : ( this . task ? . taskStatus === 'CLAIMED' ? '作业中' : '待领取' ) },
2026-09-11 13:54:06 +08:00
palletSetupReady () { return this . task ? . palletSetupReady === true },
palletSetupMessage () { return this . task ? . palletSetupMessage || `预计 ${ this . task ? . expectedPalletCount || 0 } 托,当前仅生成 ${ this . task ? . generatedPalletCount || 0 } 个托盘码,请联系后台处理后刷新` },
2026-10-19 16:40:51 +08:00
canResolveCarton () { return Boolean ( this . palletSetupReady && String ( this . boxCode || '' ). trim () && ! this . loading && ! this . submitting ) },
2026-09-11 15:38:35 +08:00
canCount () { return Boolean ( this . carton ? . canCount && this . skuCounts . length && this . skuCounts . every (( line ) => Number . isInteger ( Number ( line . actualQuantity )) && Number ( line . actualQuantity ) >= 0 && ( ! this . lineDiffers ( line ) || String ( line . discrepancyRemark || '' ). trim ()))) },
2026-09-15 11:54:24 +08:00
isResolvedPendingCarton () { return this . carton ? . countStatus === 'COUNTED_UNBOUND' },
2026-09-15 20:51:06 +08:00
canRebindResolvedCarton () { return Boolean ( this . isResolvedPendingCarton && this . carton ? . cartonId && this . assignableCurrentPallet ? . palletNo ) },
2026-10-19 16:40:51 +08:00
canSubmitCarton () { return this . currentPallet ? . status === 'SEALED' || (( this . selectedPalletNo || this . palletCode ) && ! this . currentPallet ) ? false : ( this . isResolvedPendingCarton ? this . canRebindResolvedCarton : this . canCount ) },
2026-09-15 11:54:24 +08:00
cartonActionLabel () { return this . isResolvedPendingCarton ? '确认归入当前托盘' : '确认清点' },
2026-10-19 16:40:51 +08:00
currentPalletDetails () { return this . workspace ? . pallet ? . palletNo === this . selectedPalletNo ? this . workspace . pallet : null },
currentPallet () { return this . currentPalletDetails },
palletMembers () { return this . workspace ? . members || [] },
palletCustomerCount () { return new Set ( this . palletMembers . map (( member ) => String ( member . customerId ))). size },
2026-09-15 20:51:06 +08:00
assignableCurrentPallet () { return this . currentPallet ? . status === 'OPEN' ? this . currentPallet : null }
2026-09-11 11:15:07 +08:00
},
2026-10-19 16:40:51 +08:00
onLoad ( options ) { this . taskId = String ( options . taskId || '' ); this . warehouseId = String ( options . warehouseId || '' ); this . initialPalletNo = String ( options . palletNo || '' ) },
2026-10-20 11:34:58 +08:00
onShow () { if ( ! requireSession ()) return ; this . user = session . currentUser ; const draft = restoreDropshipPalletizingDraft ( this . draftIdentity ()); this . selectedPalletNo = this . initialPalletNo || draft ? . activePalletNo || '' ; this . initialPalletNo = '' ; this . startScanner (); this . loadTask ({ restorePallet : true }) },
2026-09-11 11:15:07 +08:00
onHide () { this . stopScanner () }, onUnload () { this . stopScanner () }, beforeUnmount () { this . stopScanner () },
2026-09-15 20:51:06 +08:00
onBackPress () { if ( ! this . unsealOpen ) return false ; this . closeUnseal (); return true },
2026-09-11 11:15:07 +08:00
methods : {
2026-10-23 18:45:48 +08:00
formatOperationError ( error , fallback ) {
console . error ( '[PDA dropship inbound]' , { code : error ? . code , requestId : error ? . requestId , traceId : error ? . traceId })
return sanitizeUserMessage ( error ? . message , fallback )
},
2026-09-11 11:15:07 +08:00
startScanner () { this . stopScanner (); this . _stopScanner = startScanBroadcast ({ onScan : ( value ) => this . handleScan ( value ), onError : () => { this . error = '扫码头连接失败,可使用输入框继续作业' } }) },
stopScanner () { if ( this . _stopScanner ) this . _stopScanner (); this . _stopScanner = null },
2026-10-19 16:40:51 +08:00
handleScan ( value ) { if ( this . loading || this . submitting ) return ; if ( ! this . palletSetupReady ) { this . error = this . palletSetupMessage ; return } const plan = planDropshipScan ( createDropshipScanState ({ online : isOnline () }), value , { unboundCartons : this . task ? . unboundCartons }); if ([ DROPSHIP_SCAN_INTENT . BLOCKED , DROPSHIP_SCAN_INTENT . INVALID ]. includes ( plan . intent )) { this . error = plan . message ; return } const code = plan . palletNo || plan . boxCode ; if ( plan . palletNo ) { this . palletCode = code ; this . selectPallet ( code ) } else { this . boxCode = code ; this . resolveCarton () } },
2026-09-11 11:15:07 +08:00
resetMessages () { this . error = '' ; this . notice = '' },
2026-10-20 11:34:58 +08:00
async loadTask ({ restorePallet = false } = {}) {
2026-09-11 11:15:07 +08:00
if ( ! this . taskId || ! this . warehouseId ) { this . error = '缺少合托任务或仓库信息,请返回任务列表' ; return }
this . loading = true ; this . resetMessages ()
2026-10-20 11:34:58 +08:00
try { this . task = await getDropshipPalletizationTask ( this . taskId , this . warehouseId ); await this . refreshPallet ({ restorePallet }); if ( this . task . taskStatus === 'COMPLETED' ) clearDropshipPalletizingDraft ( this . draftIdentity ()) }
2026-10-23 18:45:48 +08:00
catch ( error ) { this . error = this . formatOperationError ( error , '一件代发入库任务加载失败' ) }
2026-09-11 11:15:07 +08:00
finally { this . loading = false }
},
2026-09-11 11:51:35 +08:00
async claimTask () { await this . runMutation (() => claimDropshipPalletizationTask ( this . taskId , this . warehouseId ), '任务领取成功' ) },
2026-09-11 11:15:07 +08:00
async resolveCarton ( value ) {
2026-10-19 16:40:51 +08:00
if ( this . loading || this . submitting ) return
2026-09-11 13:54:06 +08:00
if ( ! this . palletSetupReady ) { this . error = this . palletSetupMessage ; return }
2026-09-11 11:15:07 +08:00
const code = String ( value || this . boxCode || '' ). trim (). toUpperCase (); if ( ! code || this . loading ) return
this . boxCode = code ; this . loading = true ; this . resetMessages (); this . carton = null
2026-10-19 16:40:51 +08:00
try { const resolved = await resolveDropshipWorkspaceCarton ( this . warehouseId , code ); this . scanTask = resolved . task ; this . carton = resolved . carton ; this . skuCounts = ( this . carton . skuLines || []). map (( line ) => ({ ... line , actualQuantity : String ( line . actualQuantity ?? line . expectedQuantity ?? '' ), discrepancyRemark : line . discrepancyRemark || '' })) }
2026-10-23 18:45:48 +08:00
catch ( error ) { this . error = this . formatOperationError ( error , '箱码解析失败' ) }
2026-09-11 11:15:07 +08:00
finally { this . loading = false }
},
async countCarton () {
2026-09-11 15:38:35 +08:00
const skuLines = this . skuCounts . map (( line ) => ({ inboundDetailId : line . inboundDetailId , skuCode : line . skuCode , actualQuantity : Number ( line . actualQuantity ), discrepancyAction : this . lineDiffers ( line ) ? 'ACCEPT_VARIANCE' : 'MATCH' , discrepancyRemark : this . lineDiffers ( line ) ? String ( line . discrepancyRemark || '' ). trim () : null }))
2026-10-19 16:40:51 +08:00
const result = await this . runMutation ( async () => { await this . activateScanTask (); return countDropshipCarton ( this . taskId , { warehouseId : this . warehouseId , boxCode : this . carton . boxCode , palletNo : this . assignableCurrentPallet ? . palletNo || null , skuLines }) }, this . assignableCurrentPallet ? '本箱已清点并归托' : '本箱已完成入库清点' )
2026-09-11 15:38:35 +08:00
if ( result ) { this . boxCode = '' ; this . carton = null ; this . skuCounts = [] }
2026-09-11 11:15:07 +08:00
},
2026-09-15 11:54:24 +08:00
async confirmCarton () {
2026-10-19 16:40:51 +08:00
if ( ! this . canSubmitCarton || this . loading || this . submitting ) return
2026-09-15 11:54:24 +08:00
if ( this . isResolvedPendingCarton ) { await this . rebindResolvedCarton (); return }
await this . countCarton ()
},
async rebindResolvedCarton () {
if ( ! this . canRebindResolvedCarton ) return
2026-09-15 20:51:06 +08:00
const targetPalletNo = this . assignableCurrentPallet . palletNo
2026-09-15 11:54:24 +08:00
const payload = { warehouseId : this . warehouseId , targetPalletNo }
payload . operationId = stableOperationId ( 'dropship-move' , String ( this . user ? . id ) + '-' + this . taskId , { cartonId : this . carton . cartonId , taskVersion : this . task . version , ... payload })
2026-10-19 16:40:51 +08:00
const result = await this . runMutation ( async () => { await this . activateScanTask (); return moveDropshipCarton ( this . taskId , this . carton . cartonId , payload ) }, `本箱已归入 ${ targetPalletNo } ` )
2026-09-15 11:54:24 +08:00
if ( result ) { this . boxCode = '' ; this . carton = null ; this . skuCounts = [] }
},
2026-09-11 15:38:35 +08:00
lineDiffers ( line ) { return Number ( line . actualQuantity ) !== Number ( line . expectedQuantity ) },
adjustQuantity ( index , step ) { const line = this . skuCounts [ index ]; if ( line ) line . actualQuantity = String ( Math . max ( 0 , Number ( line . actualQuantity || 0 ) + step )) },
2026-09-11 12:12:40 +08:00
async selectPallet ( value ) {
2026-10-19 16:40:51 +08:00
if ( this . loading || this . submitting ) return
2026-09-11 13:54:06 +08:00
if ( ! this . palletSetupReady ) { this . error = this . palletSetupMessage ; return }
2026-10-19 16:40:51 +08:00
const code = String ( value || this . palletCode || '' ). trim (). toUpperCase ()
this . palletCode = code
this . loading = true ; this . resetMessages (); this . workspace = null
try { this . workspace = await resolveDropshipWorkspacePallet ( this . warehouseId , code ); this . selectedPalletNo = code ; this . palletCode = code ; this . selectedPalletId = String ( this . workspace . pallet . palletId ); this . activeScan = 'boxCode' ; saveDropshipPalletizingDraft ( this . draftIdentity (), { activePalletNo : code }) }
2026-10-23 18:45:48 +08:00
catch ( error ) { this . selectedPalletNo = '' ; this . error = this . formatOperationError ( error , '托盘校验失败' ); return }
2026-10-19 16:40:51 +08:00
finally { this . loading = false }
const pallet = this . currentPallet
2026-09-15 20:51:06 +08:00
if ( pallet ? . status === 'SEALED' ) { this . notice = `托盘 ${ code } 已封托,可在托盘操作区解除封托` ; return }
2026-09-14 11:05:48 +08:00
if ( Number ( this . task ? . unboundCartons || 0 ) > 0 ) await this . runMutation (() => bindDropshipPendingCartons ( this . taskId , this . warehouseId , code , this . task . version ), `待归托箱已绑定至 ${ code } ` )
2026-10-19 16:40:51 +08:00
else this . notice = `已选择仓库开放托盘 ${ code } ,可连续扫描不同客户的箱码`
},
2026-10-20 11:34:58 +08:00
// 页面恢复只延续开放托盘;主动扫码和刚封托后的回查仍保留真实封托结果。
async refreshPallet ({ restorePallet = false } = {}) {
this . workspace = null
if ( ! this . selectedPalletNo ) return
const workspace = await resolveDropshipWorkspacePallet ( this . warehouseId , this . selectedPalletNo )
if ( restorePallet && workspace . pallet . status !== 'OPEN' ) {
this . selectedPalletNo = '' ; this . selectedPalletId = '' ; this . palletCode = ''
clearDropshipPalletizingDraft ( this . draftIdentity ())
return
}
this . workspace = workspace
},
2026-10-19 16:40:51 +08:00
memberForCarton ( carton ) { return this . palletMembers . find (( member ) => String ( member . inboundId ) === String ( carton . inboundId )) },
async activateScanTask () {
const targetId = String ( this . scanTask ? . taskId || this . taskId )
let target = await getDropshipPalletizationTask ( targetId , this . warehouseId )
if ( target . taskStatus === 'PENDING' ) target = await claimDropshipPalletizationTask ( targetId , this . warehouseId )
this . taskId = targetId ; this . task = target
saveDropshipPalletizingDraft ( this . draftIdentity (), { activePalletNo : this . selectedPalletNo })
},
async activatePalletTask () {
const member = this . palletMembers . find (( item ) => String ( item . taskId ) === this . taskId ) || this . palletMembers [ 0 ]
if ( ! member ) throw new Error ( '托盘没有已归托箱,请先清点归托' )
const target = await getDropshipPalletizationTask ( member . taskId , this . warehouseId )
this . taskId = String ( target . taskId ); this . task = target
2026-09-11 12:12:40 +08:00
},
2026-09-14 11:05:48 +08:00
draftIdentity () { return { operatorId : this . user ? . id , taskId : this . taskId , warehouseId : this . warehouseId } },
async moveCarton ( carton , targetPalletNo ) {
if ( this . submitting || ! isOnline () || ! this . palletSetupReady ) return
const payload = { warehouseId : this . warehouseId , targetPalletNo : targetPalletNo || null }
payload . operationId = stableOperationId ( 'dropship-move' , String ( this . user ? . id ) + '-' + this . taskId , { cartonId : carton . cartonId , taskVersion : this . task . version , ... payload })
2026-10-19 16:40:51 +08:00
const member = this . memberForCarton ( carton )
if ( ! member ) { this . error = '未找到箱码所属任务,请刷新托盘' ; return }
await this . runMutation ( async () => { const target = await getDropshipPalletizationTask ( member . taskId , this . warehouseId ); this . taskId = String ( target . taskId ); this . task = target ; return moveDropshipCarton ( this . taskId , carton . cartonId , payload ) }, targetPalletNo ? '箱码已换托' : '箱码已移回待绑托区' )
},
continueOtherTask () { uni . redirectTo ({ url : `/pages/dropship-tasks/dropship-tasks?warehouseId= ${ encodeURIComponent ( this . warehouseId ) } &palletNo= ${ encodeURIComponent ( this . assignableCurrentPallet ? . palletNo || '' ) } ` }) },
async sealPallet ( pallet ) {
if ( this . loading || this . submitting ) return
const content = this . palletMembers . map (( member ) => ` ${ member . customerName || '客户未关联' } / ${ member . inboundCode } : ${ member . cartonCount } 箱` ). join ( '\n' )
uni . showModal ({ title : `确认封托 · ${ pallet . cartonCount } 箱` , content : ` ${ content } \ n请确认实物已装满,封托后按整托装柜。` , success : async ( answer ) => { if ( answer . confirm ) await this . runMutation ( async () => { await this . activatePalletTask (); return sealDropshipPallet ( this . taskId , this . warehouseId , pallet ) }, ` ${ pallet . palletNo } 已封托` ) } })
2026-09-14 11:05:48 +08:00
},
2026-09-15 20:51:06 +08:00
openUnseal ( pallet ) { this . unsealTarget = pallet ; this . unsealOpen = true ; this . $refs . unsealPopup . open () },
closeUnseal () { this . unsealOpen = false ; this . $refs . unsealPopup ? . close (); this . unsealTarget = null },
handleUnsealPopupChange ( event ) { this . unsealOpen = Boolean ( event . show ); if ( ! event . show ) this . unsealTarget = null },
async confirmUnseal () {
const pallet = this . unsealTarget ; if ( ! pallet || this . submitting ) return
this . $refs . unsealPopup . close (); this . unsealOpen = false
2026-10-19 16:40:51 +08:00
const result = await this . runMutation ( async () => { await this . activatePalletTask (); return unsealDropshipPallet ( this . taskId , this . warehouseId , pallet ) }, ` ${ pallet . palletNo } 已解除封托` )
2026-09-15 20:51:06 +08:00
this . unsealTarget = null
if ( result ) { this . selectedPalletNo = pallet . palletNo ; saveDropshipPalletizingDraft ( this . draftIdentity (), { activePalletNo : pallet . palletNo }) }
},
2026-09-11 11:15:07 +08:00
async runMutation ( action , message ) {
if ( this . submitting ) return null
2026-09-14 11:05:48 +08:00
if ( ! isOnline ()) { this . error = '当前网络不可用,请联网后刷新' ; return null }
2026-09-11 11:15:07 +08:00
this . submitting = true ; this . resetMessages ()
try {
const result = await action ()
this . task = result ? . taskId && Array . isArray ( result ? . pallets )
? result
: await getDropshipPalletizationTask ( this . taskId , this . warehouseId )
this . notice = message
2026-10-23 18:45:48 +08:00
try { await this . refreshPallet () } catch ( error ) { this . error = this . formatOperationError ( error , '操作已成功,托盘汇总刷新失败,请刷新后继续' ) }
2026-09-11 11:15:07 +08:00
return result
}
2026-10-23 18:45:48 +08:00
catch ( error ) { this . error = this . formatOperationError ( error , '操作失败' ); return null }
2026-09-11 11:15:07 +08:00
finally { this . submitting = false }
}
}
}
</ script >
< style scoped >
2026-10-19 16:40:51 +08:00
. member - row { display : flex ; justify - content : space - between ; gap : 8 px ; padding : 8 px 0 ; border - bottom : 1 px solid # edf1f5 ; font - size : 12 px ; }
. carton - owner { display : block ; color : # 667085 ; font - size : 12 px ; line - height : 20 px ; overflow - wrap : anywhere ; }
2026-09-11 14:21:46 +08:00
. content { padding : 0 0 32 px ; background : # fff ; }
2026-09-11 11:15:07 +08:00
. state , . message { margin - bottom : 10 px ; padding : 12 px ; border - radius : 9 px ; color : # 667085 ; font - size : 12 px ; line - height : 18 px ; text - align : center ; background : # fff ; }
. message . error { color : # b42318 ; background : # fff1f0 ; }. message . success { color : # 067647 ; background : # ecfdf3 ; }
2026-09-11 14:21:46 +08:00
. task - summary - card , . section - card { margin : 0 ; padding : 11 px 12 px ; border : 0 ; border - bottom : 6 px solid # eef2f7 ; border - radius : 0 ; background : # fff ; box - shadow : none ; }
2026-09-11 17:12:08 +08:00
. continue - strip { padding : 10 px 12 px 12 px ; border - bottom : 6 px solid # eef2f7 ; color : # 475467 ; font - size : 13 px ; line - height : 19 px ; background : # f8fbff ; }. continue - strip . secondary - button { margin - top : 7 px ; }
2026-09-11 11:51:35 +08:00
. task - summary - card { display : block ; }
2026-09-11 11:15:07 +08:00
. summary - head , . section - heading , . pallet - head , . carton - head { display : flex ; align - items : flex - start ; justify - content : space - between ; gap : 10 px ; }
2026-09-11 14:18:53 +08:00
. inbound - code { min - width : 0 ; color : # 12233 f ; font - size : 17 px ; font - weight : 700 ; overflow - wrap : anywhere ; }
2026-09-11 14:21:46 +08:00
. status , . pallet - status { flex - shrink : 0 ; padding : 4 px 8 px ; border - radius : 99 px ; color : # 155 eef ; font - size : 12 px ; background : # eff4ff ; }. pallet - status . sealed { color : # 067647 ; background : # ecfdf3 ; }
. warehouse - summary { display : block ; margin - top : 3 px ; color : # 667085 ; font - size : 12 px ; }
. compact - metrics { display : grid ; grid - template - columns : repeat ( 5 , 1 fr ); margin - top : 8 px ; padding - top : 7 px ; border - top : 1 px solid # edf1f5 ; }. compact - metrics > text { display : flex ; align - items : center ; justify - content : center ; gap : 3 px ; color : # 667085 ; font - size : 11 px ; white - space : nowrap ; }. compact - metrics strong { color : # 12233 f ; font - size : 15 px ; }. compact - metrics . pallet - readiness { color : # 067647 ; }. compact - metrics . pallet - readiness . invalid ,. compact - metrics . pallet - readiness . invalid strong { color : # b42318 ; }. pallet - setup - error { margin - top : 8 px ; margin - bottom : 0 ; text - align : left ; }
2026-10-22 23:06:04 +08:00
. section - title { display : block ; color : # 12233 f ; font - size : 18 px ; font - weight : 700 ; }. section - heading { align - items : center ; }. refresh - button { display : flex ; align - items : center ; justify - content : center ; flex - shrink : 0 ; box - sizing : border - box ; min - width : 88 px ; height : 44 px ; margin : 0 ; padding : 0 16 px ; border : 1 px solid # 9 dc0f4 ; border - radius : 8 px ; color : # 155 eef ; font - size : 16 px ; font - weight : 700 ; line - height : normal ; background : # eaf2ff ; }. refresh - button :: after { border : 0 ; }. refresh - button [ disabled ] { color : # 718095 ; border - color : # dce5f0 ; background : # e8edf3 ; }
2026-09-11 11:15:07 +08:00
. primary - button , . secondary - button { box - sizing : border - box ; width : 100 % ; height : 46 px ; margin - top : 10 px ; border - radius : 9 px ; font - size : 14 px ; font - weight : 700 ; line - height : 44 px ; }. primary - button { border : 0 ; color : # fff ; background : # 1 a73e8 ; }. secondary - button { border : 1 px solid # 8 ab8ef ; color : # 155 eef ; background : # f6f9ff ; }. primary - button [ disabled ],. secondary - button [ disabled ] { opacity : .45 ; }
2026-09-15 20:51:06 +08:00
. unseal - button { border - color : # fda29b ; color : # b42318 ; background : # fff5f4 ; }. pallet - hint . blocked { color : # b42318 ; }
. unseal - dialog { box - sizing : border - box ; width : min ( 350 px , calc ( 100 vw - 32 px )); overflow : hidden ; border - radius : 12 px ; background : # fff ; }. unseal - copy { display : block ; padding : 18 px 16 px ; color : # 475467 ; font - size : 14 px ; line - height : 21 px ; }. unseal - actions { display : grid ; grid - template - columns : 1 fr 1 fr ; gap : 10 px ; padding : 0 16 px 16 px ; }. unseal - actions . secondary - button { margin - top : 0 ; }. danger - button { height : 46 px ; margin : 0 ; border : 0 ; border - radius : 9 px ; color : # fff ; font - size : 14 px ; font - weight : 700 ; background : # d92d20 ; }. danger - button [ disabled ] { opacity : .45 ; }
2026-09-11 14:21:46 +08:00
. count - entry { display : flex ; align - items : center ; justify - content : space - between ; gap : 8 px ; margin - top : 6 px ; padding : 5 px 7 px ; border : 0 ; border - radius : 0 ; color : # 667085 ; font - size : 13 px ; background : # f6f9ff ; }. compact - action { width : 86 px ; height : 34 px ; margin : 0 ; border : 0 ; border - radius : 6 px ; color : # fff ; font - size : 13 px ; line - height : 34 px ; background : # 1 a73e8 ; }. compact - action [ disabled ] { opacity : .35 ; }. count - unavailable { padding : 12 px 8 px ; color : # b42318 ; font - size : 13 px ; line - height : 18 px ; text - align : center ; background : # fff1f0 ; }. pallet - scan - block { margin - top : 8 px ; padding - top : 8 px ; border - top : 1 px solid # e6edf5 ; }. field - label { display : block ; margin - bottom : 5 px ; color : # 344054 ; font - size : 15 px ; font - weight : 700 ; }. field - label text { margin - left : 3 px ; color : # 98 a2b3 ; font - size : 12 px ; font - weight : 400 ; }
2026-09-11 15:38:35 +08:00
. carton - card , . pallet - card { margin - top : 8 px ; padding : 8 px 0 0 ; border : 0 ; border - top : 1 px solid # e3eaf2 ; border - radius : 0 ; background : transparent ; }. sku - count - row { display : grid ; grid - template - columns : minmax ( 0 , 1 fr ) auto ; align - items : center ; gap : 8 px ; padding : 8 px 0 ; border - bottom : 1 px solid # edf1f5 ; }. sku - copy { min - width : 0 ; }. sku - label { display : block ; color : # 667085 ; font - size : 11 px ; }. sku - code { display : block ; margin - top : 2 px ; color : # 12233 f ; font - size : 16 px ; font - weight : 700 ; overflow - wrap : anywhere ; }. pallet - meta { display : block ; margin - top : 5 px ; color : # 667085 ; font - size : 14 px ; line - height : 20 px ; }
. quantity - stepper { display : grid ; grid - template - columns : 38 px 56 px 38 px ; overflow : hidden ; border : 1 px solid # b8c6d8 ; border - radius : 8 px ; background : # fff ; }. quantity - stepper button ,. quantity - stepper input { box - sizing : border - box ; height : 40 px ; margin : 0 ; border : 0 ; border - radius : 0 ; text - align : center ; background : # fff ; }. quantity - stepper button { color : # 155 eef ; font - size : 20 px ; line - height : 40 px ; }. quantity - stepper input { width : 56 px ; border - right : 1 px solid # dce4ed ; border - left : 1 px solid # dce4ed ; color : # 12233 f ; font - size : 15 px ; }. difference - field { grid - column : 1 / - 1 ; margin - top : 0 ; }. pallet - hint { display : block ; margin - top : 10 px ; color : # 155 eef ; font - size : 11 px ; text - align : center ; }
2026-09-15 11:54:24 +08:00
. counted - quantity { flex : 0 0 auto ; color : # 067647 ; font - size : 14 px ; font - weight : 700 ; }
2026-09-11 11:15:07 +08:00
. field { display : flex ; align - items : center ; justify - content : space - between ; gap : 12 px ; margin - top : 10 px ; color : # 344054 ; font - size : 12 px ; }. field input { box - sizing : border - box ; width : 120 px ; height : 42 px ; padding : 0 10 px ; border : 1 px solid # cfd8e3 ; border - radius : 8 px ; background : # fff ; }. field . stacked { align - items : stretch ; flex - direction : column ; gap : 6 px ; }. field textarea { box - sizing : border - box ; width : 100 % ; height : 70 px ; padding : 9 px ; border : 1 px solid # cfd8e3 ; border - radius : 8 px ; background : # fff ; }
2026-09-11 14:21:46 +08:00
. pallet - code { color : # 12233 f ; font - size : 19 px ; font - weight : 700 ; overflow - wrap : anywhere ; }. text - button { min - width : 58 px ; height : 40 px ; border : 0 ; color : # 155 eef ; font - size : 14 px ; line - height : 40 px ; background : transparent ; }. empty { padding : 18 px 0 8 px ; color : # 98 a2b3 ; font - size : 14 px ; text - align : center ; }
. palletization - page : deep (. scan - target - value ) { font - size : 17 px ; }
2026-09-11 11:15:07 +08:00
</ style >