@@ -5,24 +5,35 @@
< view class = "fulfillment-heading" > < text class = "fulfillment-title" > 理货审核 、 真实拣货与装车 < / text > < / view >
< view class = "fulfillment-heading" > < text class = "fulfillment-title" > 理货审核 、 真实拣货与装车 < / text > < / view >
< view v-if = "pendingCommand" class="fulfillment-card gate-card"><text>排车 {{ pendingCommand.dispatchNo }} 的原操作结果待核对,请保留原请求。</text><button :disabled="busy || loading" @tap="recoverCommand">核对并重试原请求</button><button v-if="pendingCommand.rejected" :disabled="busy" @tap="discardRejected" > 按明确拒绝结果重新核对 < / button > < / view >
< view v-if = "pendingCommand" class="fulfillment-card gate-card"><text>排车 {{ pendingCommand.dispatchNo }} 的原操作结果待核对,请保留原请求。</text><button :disabled="busy || loading" @tap="recoverCommand">核对并重试原请求</button><button v-if="pendingCommand.rejected" :disabled="busy" @tap="discardRejected" > 按明确拒绝结果重新核对 < / button > < / view >
< view v-if = "current" class="detail-toolbar"><text class="detail-title">{{ current.dispatchNo }}</text><button :disabled="busy" @tap="closeTask" > ‹ 返回列表 < / button > < / view >
< view v-if = "current" class="detail-toolbar"><text class="detail-title">{{ current.dispatchNo }}</text><button :disabled="busy" @tap="closeTask" > ‹ 返回列表 < / button > < / view >
< view class = "fulfillment-card" > < ScanTargetInput name = "dispatch" :model-value = "scanCode" label = "排车单号/实物托盘码/来源箱唛" placeholder = "扫描业务单号或实物码" :active = "true" @ update : modelValue = "scanCode = $event" @submit ="handleScan" / > < button : disabled = "busy || loading || !scanCode || Boolean(pendingCommand)" @tap ="handleScan(scanCode)" > 匹配扫描 < / button > < / view >
< view v-if = "current" class="stage-switch" role="tablist" aria-label="排车作业" >
< button id = "dispatch-picking-tab" class = "stage-tab" : class = "{ active: activeTab === 'picking' }" role = "tab" : aria -selected = " activeTab = = = ' picking ' " aria -controls = " dispatch -picking -panel " : disabled = "busy || loading || Boolean(pendingCommand)" @tap ="selectTab('picking')" > 拣货作业 < / button >
< button id = "dispatch-photos-tab" class = "stage-tab" : class = "{ active: activeTab === 'photos' }" role = "tab" : aria -selected = " activeTab = = = ' photos ' " aria -controls = " dispatch -photos -panel " : disabled = "!canShowPhotos || busy || loading || Boolean(pendingCommand)" @tap ="selectTab('photos')" > 装车拍照 < / button >
< / view >
< view v-if = "!current || activeTab === 'picking'" class="fulfillment-card"><ScanTargetInput name="dispatch" :model-value="scanCode" label="排车单号/实物托盘码/来源箱唛" placeholder="扫描业务单号或实物码" :active="activeTab === 'picking'" @update:modelValue="scanCode = $event" @submit="handleScan" /><button :disabled="busy || loading || !scanCode || Boolean(pendingCommand)" @tap="handleScan(scanCode)" > 匹配扫描 < / button > < / view >
< view v-if = "loading" class="fulfillment-state" > < text > 正在加载排车事实 … < / text > < / view >
< view v-if = "loading" class="fulfillment-state" > < text > 正在加载排车事实 … < / text > < / view >
< view v-if = "error" class="fulfillment-error" role="alert"><text class="state-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><text v-if="error.network">网络中断,原请求和作业草稿已保留。</text><button :disabled="busy || loading" @tap="retry" > 重新核对 < / button > < / view >
< view v-if = "error" class="fulfillment-error" role="alert"><text class="state-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><text v-if="error.network">网络中断,原请求和作业草稿已保留。</text><button :disabled="busy || loading" @tap="retry" > 重新核对 < / button > < / view >
< template v-if = "!current && !loading"><view class="list-toolbar"><text class="list-title">排车任务</text><button :disabled="busy" @tap="loadTasks">刷新</button></view><view v-if="!tasks.length && !error" class="fulfillment-state">暂无排车任务,可扫描排车单号读取。</view><button v-for="task in tasks" :key="task.dispatchNo" class="task-card" @tap="openTask(task.dispatchNo)"><text class="business-code">{{ task.dispatchNo }}</text><text v-if="task.dispatchNo === recentTaskNo" class="recent-badge">上次作业</text><text class="status-badge">{{ stageLabel(task) }}</text><text class="task-meta" > 任务号 {{ task.taskNo | | ' — ' }} < / text > < / button > < / template >
< template v-if = "!current && !loading"><view class="list-toolbar"><text class="list-title">排车任务</text><button :disabled="busy" @tap="loadTasks">刷新</button></view><view v-if="!tasks.length && !error" class="fulfillment-state">暂无排车任务,可扫描排车单号读取。</view><button v-for="task in tasks" :key="task.dispatchNo" class="task-card" @tap="openTask(task.dispatchNo)"><text class="business-code">{{ task.dispatchNo }}</text><text v-if="task.dispatchNo === recentTaskNo" class="recent-badge">上次作业</text><text class="status-badge">{{ stageLabel(task) }}</text><text class="task-meta" > 任务号 {{ task.taskNo | | ' — ' }} < / text > < / button > < / template >
< template v-if = "current"><DispatchLoadingDocuments v-if="['LOADING', 'AFTER_LOADING', 'DELIVERING', 'DELIVERED'].includes(current.stage)" :key="current.dispatchNo" :dispatch-no="current.dispatchNo" @busy-change="busy = $event" @saved="openTask(current.dispatchNo)" /><DispatchHandoverStatus v-if="departed" :dispatch-no="current.dispatchNo" :record-version="current.recordVersion" / >
< template v-if = "current" >
< view v-show = "activeTab === 'photos'" id="dispatch-photos-panel" role="tabpanel" aria-labelledby="dispatch-photos-tab"><DispatchLoadingDocuments v-if="canShowPhotos" :key="current.dispatchNo" :dispatch-no="current.dispatchNo" @busy-change="busy = $event" @saved="openTask(current.dispatchNo)" / > < / view >
< view v-show = "activeTab === 'picking'" id="dispatch-picking-panel" role="tabpanel" aria-labelledby="dispatch-picking-tab" >
< DispatchHandoverStatus v-if = "departed" :dispatch-no="current.dispatchNo" :record-version="current.recordVersion" / >
< view class = "fulfillment-card summary-card" > < view class = "task-head" > < text class = "business-code" > { { current . dispatchNo } } < / text > < text class = "status-badge" > { { stageLabel ( current ) } } < / text > < / view > < text class = "task-meta" > 任务号 { { current . taskNo || '—' } } < / text > < text v-if = "!gate('PICK') && current.stage === 'LOADING'" class="gate-warning">{{ current.actions.PICK?.blockReason || '拣货资格尚未允许,请核对两级审核及当前作业状态' }}</text><view class="metrics"><view><text class="metric-value">{{ plannedPackages }}</text><text>计划箱数</text></view><view><text class="metric-value">{{ pickedPackages }}</text><text>真实已拣</text></view><view><text class="metric-value" > {{ remainingPackages }} < / text > < text > 未拣箱数 < / text > < / view > < / view > < / view >
< view class = "fulfillment-card summary-card" > < view class = "task-head" > < text class = "business-code" > { { current . dispatchNo } } < / text > < text class = "status-badge" > { { stageLabel ( current ) } } < / text > < / view > < text class = "task-meta" > 任务号 { { current . taskNo || '—' } } < / text > < text v-if = "!gate('PICK') && current.stage === 'LOADING'" class="gate-warning">{{ current.actions.PICK?.blockReason || '拣货资格尚未允许,请核对两级审核及当前作业状态' }}</text><view class="metrics"><view><text class="metric-value">{{ plannedPackages }}</text><text>计划箱数</text></view><view><text class="metric-value">{{ pickedPackages }}</text><text>真实已拣</text></view><view><text class="metric-value" > {{ remainingPackages }} < / text > < text > 未拣箱数 < / text > < / view > < / view > < / view >
< view v-if = "current.stage === 'BEFORE_LOADING'" class="fulfillment-card review-card" >
< view v-if = "current.stage === 'BEFORE_LOADING'" class="fulfillment-card review-card" >
< text class = "card-title" > 理货员审核 < / text >
< text class = "card-title" > 理货员审核 < / text >
< text class = "task-meta" > 车牌号 : { { current . resources ? . vehicle ? . plate || '—' } } · 托车公司 : { { current . resources ? . vehicle ? . carrierName || '—' } } < / text >
< text class = "task-meta" > 车牌号 : { { current . resources ? . vehicle ? . plate || '—' } } · 托车公司 : { { current . resources ? . vehicle ? . carrierName || '—' } } < / text >
< text class = "task-meta" > 司机 : { { current . resources ? . driver ? . name || '—' } } · 电话 : { { current . resources ? . driver ? . phone || '—' } } < / text >
< text class = "task-meta" > 司机 : { { current . resources ? . driver ? . name || '—' } } · 电话 : { { current . resources ? . driver ? . phone || '—' } } < / text >
< text class = "task-meta" > 计划发车 ( 北京时间 ) : { { reviewTime ( current . plannedDepartureAt ) } } < / text >
< text class = "task-meta" > 预计送达 ( 北京时间 ) : { { reviewTime ( current . estimatedArrivalAt ) } } < / text >
< text v-for = "(appointment, index) in current.fbaAppointments || []" :key="appointment.batchKey + '-' + index" class="task-meta" > {{ appointment.destinationWarehouseCode }} · {{ appointment.appointmentNo | | appointment.batchNo | | ' — ' }} · {{ appointment.status = = = ' SUCCESS ' ? ' 已确认 ' : ' 未确认 ' }} : {{ reviewTime ( appointment.confirmedAt ) }} ( 北京时间 ) < / text >
< text class = "task-meta" > 驾驶证号 : { { current . resources ? . driver ? . evidence ? . licenseNo || '—' } } · 有效期 : { { current . resources ? . driver ? . evidence ? . licenseExpiresOn || '—' } } < / text >
< text class = "task-meta" > 驾驶证号 : { { current . resources ? . driver ? . evidence ? . licenseNo || '—' } } · 有效期 : { { current . resources ? . driver ? . evidence ? . licenseExpiresOn || '—' } } < / text >
< text class = "task-meta" > 调度审核 : { { reviewLabel ( current . reviews ? . dispatchReview ) } } · { { current . reviews ? . dispatchReview ? . reviewedAt || '—' } } < / text >
< text class = "task-meta" > 调度审核 : { { reviewLabel ( current . reviews ? . dispatchReview ) } } · { { current . reviews ? . dispatchReview ? . reviewedAt || '—' } } < / text >
< text v-if = "current.reviews?.dispatchReview?.reason" class="task-meta" > 调度意见 : {{ current.reviews.dispatchReview.reason }} < / text >
< text v-if = "current.reviews?.dispatchReview?.reason" class="task-meta" > 调度意见 : {{ current.reviews.dispatchReview.reason }} < / text >
< text v-if = "rejectionReason" class="gate-warning" > 已退回客服处理 ( 车辆信息 ) : {{ rejectionReason }} 。 修改后须重新提交审核 。 < / text >
< text v-if = "rejectionReason" class="gate-warning" > 已退回客服处理 ( 车辆信息 ) : {{ rejectionReason }} 。 修改后须重新提交审核 。 < / text >
< template v-if = "canReview" >
< template v-if = "canReview" >
< text v-if = "current.actions.LOADING_REVIEW?.blockCode === 'APPROVAL_BLOCKED'" class="gate-warning" > {{ current.actions.LOADING_REVIEW.blockReason }} < / text >
< text class = "task-meta" > 请核对车辆 、 司机及下方货物 ; 通过后才能开始真实拣货 。 驳回将回到客服处理 ( 车辆信息 ) 。 < / text >
< text class = "task-meta" > 请核对车辆 、 司机及下方货物 ; 通过后才能开始真实拣货 。 驳回将回到客服处理 ( 车辆信息 ) 。 < / text >
< textarea :value = "reviewReason" maxlength = "1000" placeholder = "审核意见(驳回时必填)" : disabled = "busy || loading || Boolean(pendingCommand)" @input ="updateReviewReason" / >
< textarea :value = "reviewReason" maxlength = "1000" placeholder = "审核意见(驳回时必填)" : disabled = "busy || loading || Boolean(pendingCommand)" @input ="updateReviewReason" / >
< view class = "count-row" > < button : disabled = "busy || loading || Boolean(pendingCommand) || !reviewReason.trim()" @tap ="submitReview('REJECTED')" > 驳回至客服处理 < / button > < button class = "primary-button" : disabled = "busy || loading || Boolean(pendingCommand)" @tap ="submitReview('APPROVED')" > 审核通过 < / button > < / view >
< view class = "count-row" > < button : disabled = "busy || loading || Boolean(pendingCommand) || !reviewReason.trim()" @tap ="submitReview('REJECTED')" > 驳回至客服处理 < / button > < button class = "primary-button" : disabled = "busy || loading || Boolean(pendingCommand) || current.actions.LOADING_REVIEW?.blockCode === 'APPROVAL_BLOCKED' " @tap ="submitReview('APPROVED')" > 审核通过 < / button > < / view >
< / template >
< / template >
< text v-else-if = "!rejectionReason" class="gate-warning" > {{ current.actions.LOADING_REVIEW ? .blockReason | | ' 当前不具备理货审核资格 , 请先完成客服提交和调度审核 ' }} < / text >
< text v-else-if = "!rejectionReason" class="gate-warning" > {{ current.actions.LOADING_REVIEW ? .blockReason | | ' 当前不具备理货审核资格 , 请先完成客服提交和调度审核 ' }} < / text >
< / view >
< / view >
@@ -31,10 +42,18 @@
< view v-if = "current.stage === 'LOADING' && gate('PICK') && selectedItem && selectedItem.scheduledPackages > selectedItem.pickedPackages && selectedRemaining === 0" class="fulfillment-card gate-card"><text class="gate-warning">该托盘暂无可拣来源,请核对是否已完成实收,或货物是否冻结、被占用及来源发生变化。</text><button :disabled="busy || loading || Boolean(pendingCommand)" @tap="retry" > 刷新托盘实收信息 < / button > < / view >
< view v-if = "current.stage === 'LOADING' && gate('PICK') && selectedItem && selectedItem.scheduledPackages > selectedItem.pickedPackages && selectedRemaining === 0" class="fulfillment-card gate-card"><text class="gate-warning">该托盘暂无可拣来源,请核对是否已完成实收,或货物是否冻结、被占用及来源发生变化。</text><button :disabled="busy || loading || Boolean(pendingCommand)" @tap="retry" > 刷新托盘实收信息 < / button > < / view >
< view v-if = "sourceChoices.length > 1" class="fulfillment-card"><text class="card-title">请选择本次真实拣货来源</text><button v-for="item in sourceChoices" :key="item.dispatchItemId" :disabled="busy || Boolean(pendingCommand)" @tap="chooseSource(item)" > {{ item.sourceBusinessNo | | ' — ' }} · {{ item.customerName | | ' — ' }} · 可拣 {{ pickablePackages ( item ) }} 箱 < / button > < / view >
< view v-if = "sourceChoices.length > 1" class="fulfillment-card"><text class="card-title">请选择本次真实拣货来源</text><button v-for="item in sourceChoices" :key="item.dispatchItemId" :disabled="busy || Boolean(pendingCommand)" @tap="chooseSource(item)" > {{ item.sourceBusinessNo | | ' — ' }} · {{ item.customerName | | ' — ' }} · 可拣 {{ pickablePackages ( item ) }} 箱 < / button > < / view >
< view v-if = "current.stage === 'LOADING' && gate('PICK') && selectedItem && selectedRemaining > 0" class="fulfillment-card"><text class="card-title">本次拣货:{{ selectedItem.palletNo || '来源箱唛' }}</text><text v-if="selectedItem.returnCartons.length">{{ selectedReturnCarton ? `已扫 ${selectedReturnCarton.cartonMark}` : '请扫描来源箱唛,每次一箱' }}</text><view v-else class="count-row"><text>拣货箱数</text><input type="number" :value="packages" :placeholder="`请输入 1-${selectedRemaining} 箱`" :disabled="busy || Boolean(pendingCommand)" @input="updatePackages" /><text>最多{{ selectedRemaining }}箱</text></view><button class="primary-button" :disabled="!canPick || busy || Boolean(pendingCommand)" @tap="submitPick" > 确认真实拣货 < / button > < / view >
< view v-if = "current.stage === 'LOADING' && gate('PICK') && selectedItem && selectedRemaining > 0" class="fulfillment-card"><text class="card-title">本次拣货:{{ selectedItem.palletNo || '来源箱唛' }}</text><text v-if="selectedItem.returnCartons.length">{{ selectedReturnCarton ? `已扫 ${selectedReturnCarton.cartonMark}` : '请扫描来源箱唛,每次一箱' }}</text><view v-else class="count-row"><text>拣货箱数</text><input type="number" :value="packages" :placeholder="`请输入 1-${selectedRemaining} 箱`" :disabled="busy || Boolean(pendingCommand)" @input="updatePackages" /><text>最多{{ selectedRemaining }}箱</text></view><button class="primary-button" :disabled="!canPick || busy || Boolean(pendingCommand)" @tap="submitPick" > 确认真实拣货 < / button > < / view >
< view v-if = "current.stage === 'LOADING'" class="fulfillment-card gate-card"><text class="card-title">确认装车完成</text><text>{{ current.pickingReconciled ? 'PDA拣货已核平,可边装车边上传5张装车照片;确认装车完成且照片齐全后自动进入派送中。' : '先完成全部真实拣货,不能补录数量代替核平。' }}</text><text v-if="!gate('LOADING_COMPLETE')" class="gate-warning">{{ current.actions.LOADING_COMPLETE?.blockReason || '尚未获得装车完成资格' }}</text><button class="primary-button" :disabled="!canComplete || busy || Boolean(pendingCommand)" @tap="prepareLoading" > 核对实载并 确认装车完成< / button > < / view >
< view v-if = "current.stage === 'LOADING'" class="fulfillment-card gate-card"><text class="card-title">确认装车完成</text><text>{{ current.pickingReconciled ? 'PDA拣货已核平,可边装车边上传5张装车照片;确认装车完成且照片齐全后自动进入派送中。' : '先完成全部真实拣货,不能补录数量代替核平。' }}</text><text v-if="!gate('LOADING_COMPLETE')" class="gate-warning">{{ current.actions.LOADING_COMPLETE?.blockReason || '尚未获得装车完成资格' }}</text><button class="primary-button" :disabled="!canComplete || busy || Boolean(pendingCommand)" @tap="prepareLoading" > 确认装车完成 < / button > < / view >
< / view >
< / template > < view v-if = "successMessage" class="fulfillment-success" role="status" > {{ successMessage }} < / view >
< / template > < view v-if = "successMessage" class="fulfillment-success" role="status" > {{ successMessage }} < / view >
< / view > < / scroll -view >
< / view > < / scroll -view >
< view v-if = "loadingPreview" class="loading-overlay"><view class="loading-dialog" role="dialog" aria-label="核对真实装载"><text class="card-title">核对真实装载 · {{ loadingPreview.dispatchNo }}</text><scroll-view scroll-y class="loading-dialog-body"><view v-for="row in loadingRows" :key="row.warehousePalletId" class="fulfillment-card"><text class="business-code">{{ row.palletNo || '—' }}</text><text>实际托位</text><input type="number" :value="row.palletSpaces ?? ''" placeholder="请确认实际托位" :disabled="busy" @input="setMeasurement(row, 'palletSpaces', $event)" /><text>实测重量(kg)</text><input type="digit" :value="row.weightKg ?? ''" placeholder="请输入实测重量,参考值不自动确认" :disabled="busy" @input="setMeasurement(row, 'weightKg', $event)" /><text>实测体积(m³)</text><input type="digit" :value="row.volumeCbm ?? ''" placeholder="请输入实测体积" :disabled="busy" @input="setMeasurement(row, 'volumeCbm', $event)" /><text>已匹配卸货顺序:{{ row.deliveryIndex ?? '尚未匹配,请后台核对' }}</text></view><view v-for="row in loadingPreview.pallets" :key="row.warehousePalletId"><text v-for="evidence in row.evidence" :key="evidence.field + evidence.sourceType">{{ evidence.actual ? '真实测量依据' : '仅供参考' }} {{ evidence.sourceBusinessNo || '—' }}: {{ evidence.value ?? '—' }}</text></view></scroll-view><view class="count-row"><button :disabled="busy" @tap="loadingPreview = null">返回</button><button :disabled="busy || Boolean(pendingCommand)" @tap="completeLoading" > 确认实际装车完成 < / button > < / view > < / view > < / view >
< view v-if = "loadingPreview" class="loading-overlay" >
< view class = "loading-dialog" role = "dialog" aria -modal = " true " aria -labelledby = " loading -confirm -title " >
< view class = "loading-dialog-heading" > < text id = "loading-confirm-title" class = "loading-dialog-title" > 确认装车完成 < / text > < text class = "loading-dialog-number" > { { loadingPreview . dispatchNo } } < / text > < / view >
< view class = "loading-counts" > < view class = "loading-count" > < text class = "loading-count-value" > { { loadingRows . length } } < text class = "loading-count-unit" > 托 < / text > < / text > < text class = "loading-count-label" > 已拣托盘 < / text > < / view > < view class = "loading-count" > < text class = "loading-count-value" > { { loadingRows . reduce ( ( sum , row ) => sum + Number ( row . pickedPackages || 0 ) , 0 ) } } < text class = "loading-count-unit" > 箱 < / text > < / text > < text class = "loading-count-label" > 已拣箱数 < / text > < / view > < / view >
< text class = "loading-confirm-question" > 本车货物已全部装车 ? < / text > < text class = "loading-confirm-note" > 照片可在 “ 装车拍照 ” 中继续上传 < / text >
< view class = "loading-dialog-actions" > < button class = "loading-cancel" :disabled = "busy" @tap ="loadingPreview = null" > 取消 < / button > < button class = "loading-confirm" : disabled = "busy || Boolean(pendingCommand)" :loading = "busy" @tap ="completeLoading" > 确认装车 < / button > < / view >
< / view >
< / view >
< / view >
< / view >
< / template >
< / template >
< script >
< script >
@@ -50,7 +69,7 @@ import { toDispatchError } from '@/utils/dispatch-error.js'
import { startScanBroadcast } from '@/utils/scan-broadcast.js'
import { startScanBroadcast } from '@/utils/scan-broadcast.js'
export default {
export default {
components : { PortalHeader , ScanTargetInput , DispatchHandoverStatus , DispatchLoadingDocuments } ,
components : { PortalHeader , ScanTargetInput , DispatchHandoverStatus , DispatchLoadingDocuments } ,
data ( ) { return { user : session . currentUser , tasks : [ ] , current : null , scanCode : '' , loading : false , busy : false , error : null , successMessage : '' , bootstrapped : false , resumeOnEntry : false , generation : 0 , loadingPreview : null , loadingRows : [ ] , sourceChoices : [ ] , choiceCartonCode : '' , reviewReason : '' } } ,
data ( ) { return { user : session . currentUser , tasks : [ ] , current : null , activeTab : 'picking' , scanCode : '' , loading : false , busy : false , error : null , successMessage : '' , bootstrapped : false , resumeOnEntry : false , generation : 0 , loadingPreview : null , loadingRows : [ ] , sourceChoices : [ ] , choiceCartonCode : '' , reviewReason : '' } } ,
computed : {
computed : {
recentTaskNo ( ) { return fulfillment . dispatch . currentNo || '' } , pendingCommand ( ) { return fulfillment . dispatch . pendingCommand } , packages ( ) { return fulfillment . dispatch . packages } ,
recentTaskNo ( ) { return fulfillment . dispatch . currentNo || '' } , pendingCommand ( ) { return fulfillment . dispatch . pendingCommand } , packages ( ) { return fulfillment . dispatch . packages } ,
selectedItem ( ) { return this . current ? . items . find ( row => row . dispatchItemId === fulfillment . dispatch . dispatchItemId && row . sourceKey === fulfillment . dispatch . sourceKey ) || null } ,
selectedItem ( ) { return this . current ? . items . find ( row => row . dispatchItemId === fulfillment . dispatch . dispatchItemId && row . sourceKey === fulfillment . dispatch . sourceKey ) || null } ,
@@ -58,6 +77,7 @@ export default {
selectedPosition ( ) { return this . selectedItem && this . selectedItem . scheduledPackages > this . selectedItem . pickedPackages ? this . selectedItem . positions . find ( row => row . availablePackages > 0 ) || null : null } ,
selectedPosition ( ) { return this . selectedItem && this . selectedItem . scheduledPackages > this . selectedItem . pickedPackages ? this . selectedItem . positions . find ( row => row . availablePackages > 0 ) || null : null } ,
selectedRemaining ( ) { const remaining = Math . max ( 0 , ( this . selectedItem ? . scheduledPackages || 0 ) - ( this . selectedItem ? . pickedPackages || 0 ) ) ; return this . selectedItem ? . returnCartons . length ? remaining : Math . min ( remaining , this . selectedPosition ? . availablePackages || 0 ) } ,
selectedRemaining ( ) { const remaining = Math . max ( 0 , ( this . selectedItem ? . scheduledPackages || 0 ) - ( this . selectedItem ? . pickedPackages || 0 ) ) ; return this . selectedItem ? . returnCartons . length ? remaining : Math . min ( remaining , this . selectedPosition ? . availablePackages || 0 ) } ,
plannedPackages ( ) { return ( this . current ? . items || [ ] ) . reduce ( ( sum , row ) => sum + row . scheduledPackages , 0 ) } , pickedPackages ( ) { return ( this . current ? . items || [ ] ) . reduce ( ( sum , row ) => sum + row . pickedPackages , 0 ) } , remainingPackages ( ) { return Math . max ( 0 , this . plannedPackages - this . pickedPackages ) } ,
plannedPackages ( ) { return ( this . current ? . items || [ ] ) . reduce ( ( sum , row ) => sum + row . scheduledPackages , 0 ) } , pickedPackages ( ) { return ( this . current ? . items || [ ] ) . reduce ( ( sum , row ) => sum + row . pickedPackages , 0 ) } , remainingPackages ( ) { return Math . max ( 0 , this . plannedPackages - this . pickedPackages ) } ,
canShowPhotos ( ) { return [ 'LOADING' , 'AFTER_LOADING' , 'DELIVERING' , 'DELIVERED' ] . includes ( this . current ? . stage ) } ,
departed ( ) { return [ 'DELIVERING' , 'DELIVERED' ] . includes ( this . current ? . stage ) } ,
departed ( ) { return [ 'DELIVERING' , 'DELIVERED' ] . includes ( this . current ? . stage ) } ,
canPick ( ) { return this . current ? . status === 'ACTIVE' && this . current ? . stage === 'LOADING' && this . gate ( 'PICK' ) && this . selectedRemaining > 0 && ( this . selectedItem ? . returnCartons . length ? Boolean ( this . selectedReturnCarton ) : Boolean ( this . selectedPosition ) && Number . isInteger ( Number ( this . packages ) ) && Number ( this . packages ) > 0 && Number ( this . packages ) <= this . selectedRemaining ) } ,
canPick ( ) { return this . current ? . status === 'ACTIVE' && this . current ? . stage === 'LOADING' && this . gate ( 'PICK' ) && this . selectedRemaining > 0 && ( this . selectedItem ? . returnCartons . length ? Boolean ( this . selectedReturnCarton ) : Boolean ( this . selectedPosition ) && Number . isInteger ( Number ( this . packages ) ) && Number ( this . packages ) > 0 && Number ( this . packages ) <= this . selectedRemaining ) } ,
canComplete ( ) { return this . current ? . status === 'ACTIVE' && this . current ? . stage === 'LOADING' && this . current . pickingReconciled === true && this . plannedPackages > 0 && this . remainingPackages === 0 && this . gate ( 'LOADING_COMPLETE' ) } ,
canComplete ( ) { return this . current ? . status === 'ACTIVE' && this . current ? . stage === 'LOADING' && this . current . pickingReconciled === true && this . plannedPackages > 0 && this . remainingPackages === 0 && this . gate ( 'LOADING_COMPLETE' ) } ,
@@ -68,15 +88,16 @@ export default {
onShow ( ) { if ( ! requireSession ( ) ) return ; this . user = session . currentUser ; restoreFulfillmentDraft ( ` ${ this . user ? . id || '' } : ${ this . user ? . warehouseId || '' } ` ) ; this . startScanner ( ) ; if ( ! this . bootstrapped ) { this . bootstrapped = true ; if ( this . resumeOnEntry && fulfillment . dispatch . currentNo ) this . openTask ( fulfillment . dispatch . currentNo ) ; else this . loadTasks ( ) } else if ( this . current ? . dispatchNo ) this . openTask ( this . current . dispatchNo ) ; else this . loadTasks ( ) } ,
onShow ( ) { if ( ! requireSession ( ) ) return ; this . user = session . currentUser ; restoreFulfillmentDraft ( ` ${ this . user ? . id || '' } : ${ this . user ? . warehouseId || '' } ` ) ; this . startScanner ( ) ; if ( ! this . bootstrapped ) { this . bootstrapped = true ; if ( this . resumeOnEntry && fulfillment . dispatch . currentNo ) this . openTask ( fulfillment . dispatch . currentNo ) ; else this . loadTasks ( ) } else if ( this . current ? . dispatchNo ) this . openTask ( this . current . dispatchNo ) ; else this . loadTasks ( ) } ,
onHide ( ) { this . stopScanner ( ) ; this . generation ++ } , beforeUnmount ( ) { this . stopScanner ( ) ; this . generation ++ } ,
onHide ( ) { this . stopScanner ( ) ; this . generation ++ } , beforeUnmount ( ) { this . stopScanner ( ) ; this . generation ++ } ,
methods : {
methods : {
selectTab ( tab ) { if ( this . busy || this . loading || this . pendingCommand || ! [ 'picking' , 'photos' ] . includes ( tab ) || ( tab === 'photos' && ! this . canShowPhotos ) ) return ; this . activeTab = tab } ,
gate ( action ) { return this . current ? . actions ? . [ action ] ? . allowed === true } ,
gate ( action ) { return this . current ? . actions ? . [ action ] ? . allowed === true } ,
startScanner ( ) { this . stopScanner ( ) ; this . _stopScanner = startScanBroadcast ( { onScan : value => this . handleScan ( value ) , onError : error => this . setError ( error , '扫码广播注册失败' ) } ) } , stopScanner ( ) { this . _stopScanner ? . ( ) ; this . _stopScanner = null } ,
startScanner ( ) { this . stopScanner ( ) ; this . _stopScanner = startScanBroadcast ( { onScan : value => this . handleScan ( value ) , onError : error => this . setError ( error , '扫码广播注册失败' ) } ) } , stopScanner ( ) { this . _stopScanner ? . ( ) ; this . _stopScanner = null } ,
updatePackages ( event ) { if ( this . pendingCommand ) return ; const value = Number ( event . detail . value ) ; updateFulfillmentDraft ( 'dispatch' , { packages : Number . isFinite ( value ) ? value : 0 } ) } ,
updatePackages ( event ) { if ( this . pendingCommand ) return ; const value = Number ( event . detail . value ) ; updateFulfillmentDraft ( 'dispatch' , { packages : Number . isFinite ( value ) ? value : 0 } ) } ,
async loadTasks ( ) { const generation = ++ this . generation ; await this . run ( async ( ) => { const result = await listDispatchTasks ( ) ; if ( generation === this . generation ) { this . tasks = result ; this . current = null } } , true ) } ,
async loadTasks ( ) { const generation = ++ this . generation ; await this . run ( async ( ) => { const result = await listDispatchTasks ( ) ; if ( generation === this . generation ) { this . tasks = result ; this . current = null } } , true ) } ,
async openTask ( no ) { if ( this . pendingCommand && this . pendingCommand . dispatchNo !== no ) return this . setError ( new Error ( '请先核对原排车请求,不能切到另一张单执行' ) , '操作未完成' ) ; const generation = ++ this . generation ; await this . run ( async ( ) => { const value = await getDispatchTask ( no ) ; if ( generation !== this . generation ) return ; if ( value . dispatchNo !== no ) throw new Error ( '返回排车与扫描业务号不一致' ) ; const same = fulfillment . dispatch . currentNo === no ; if ( this . current ? . dispatchNo !== no || this . current ? . recordVersion !== value . recordVersion ) this . reviewReason = '' ; this . current = value ; this . sourceChoices = [ ] ; this . choiceCartonCode = '' ; updateFulfillmentDraft ( 'dispatch' , { currentNo : no , palletCode : same ? fulfillment . dispatch . palletCode : '' , dispatchItemId : same ? fulfillment . dispatch . dispatchItemId : '' , sourceKey : same ? fulfillment . dispatch . sourceKey : '' , cartonCode : same ? fulfillment . dispatch . cartonCode : '' , packages : same ? fulfillment . dispatch . packages : 1 } ) ; this . scanCode = '' } , true ) } ,
async openTask ( no ) { if ( this . pendingCommand && this . pendingCommand . dispatchNo !== no ) return this . setError ( new Error ( '请先核对原排车请求,不能切到另一张单执行' ) , '操作未完成' ) ; const generation = ++ this . generation ; await this . run ( async ( ) => { const value = await getDispatchTask ( no ) ; if ( generation !== this . generation ) return ; if ( value . dispatchNo !== no ) throw new Error ( '返回排车与扫描业务号不一致' ) ; const same = fulfillment . dispatch . currentNo === no ; if ( this . current ? . dispatchNo !== no || this . current ? . recordVersion !== value . recordVersion ) this . reviewReason = '' ; if ( this . current ? . dispatchNo !== no || ! [ 'LOADING' , 'AFTER_LOADING' , 'DELIVERING' , 'DELIVERED' ] . includes ( value . stage ) ) this . activeTab = 'picking' ; this . current = value ; this . sourceChoices = [ ] ; this . choiceCartonCode = '' ; updateFulfillmentDraft ( 'dispatch' , { currentNo : no , palletCode : same ? fulfillment . dispatch . palletCode : '' , dispatchItemId : same ? fulfillment . dispatch . dispatchItemId : '' , sourceKey : same ? fulfillment . dispatch . sourceKey : '' , cartonCode : same ? fulfillment . dispatch . cartonCode : '' , packages : same ? fulfillment . dispatch . packages : 1 } ) ; this . scanCode = '' } , true ) } ,
closeTask ( ) { if ( this . busy ) return ; this . current = null ; this . reviewReason = '' ; this . loadingPreview = null ; this . scanCode = '' ; this . error = null ; this . successMessage = '' ; updateFulfillmentDraft ( 'dispatch' , { currentNo : '' , palletCode : '' , dispatchItemId : '' , sourceKey : '' , cartonCode : '' , packages : 1 } ) ; this . loadTasks ( ) } ,
closeTask ( ) { if ( this . busy ) return ; this . current = null ; this . activeTab = 'picking' ; this . reviewReason = '' ; this . loadingPreview = null ; this . scanCode = '' ; this . error = null ; this . successMessage = '' ; updateFulfillmentDraft ( 'dispatch' , { currentNo : '' , palletCode : '' , dispatchItemId : '' , sourceKey : '' , cartonCode : '' , packages : 1 } ) ; this . loadTasks ( ) } ,
handleScan ( raw ) {
handleScan ( raw ) {
const code = String ( raw ? . detail ? . value ? ? raw ? ? this . scanCode ) . trim ( )
const code = String ( raw ? . detail ? . value ? ? raw ? ? this . scanCode ) . trim ( )
if ( ! code || this . busy || this . loading ) return
if ( ! code || this . busy || this . loading || this . activeTab !== 'picking' ) return
if ( this . pendingCommand ) return this . setError ( new Error ( '请先核对原排车操作结果' ) , '原请求未完成' )
if ( this . pendingCommand ) return this . setError ( new Error ( '请先核对原排车操作结果' ) , '原请求未完成' )
this . error = null ; this . successMessage = '' ; this . sourceChoices = [ ] ; this . choiceCartonCode = ''
this . error = null ; this . successMessage = '' ; this . sourceChoices = [ ] ; this . choiceCartonCode = ''
updateFulfillmentDraft ( 'dispatch' , { palletCode : '' , dispatchItemId : '' , sourceKey : '' , cartonCode : '' , packages : 1 } )
updateFulfillmentDraft ( 'dispatch' , { palletCode : '' , dispatchItemId : '' , sourceKey : '' , cartonCode : '' , packages : 1 } )
@@ -110,12 +131,12 @@ export default {
this . sourceChoices = [ ] ; this . choiceCartonCode = '' ; this . error = null
this . sourceChoices = [ ] ; this . choiceCartonCode = '' ; this . error = null
} ,
} ,
async submitPick ( ) { if ( ! this . canPick || this . pendingCommand ) return ; try { const body = { recordVersion : requireDispatchVersion ( this . current . recordVersion ) , positionId : this . selectedReturnCarton ? null : this . selectedPosition . positionId , packages : this . selectedReturnCarton ? 1 : Number ( this . packages ) , productQuantity : this . selectedReturnCarton ? . productQuantity || 0 , cartonMark : this . selectedReturnCarton ? . cartonMark || null , remark : null } ; beginDispatchCommand ( this . current . dispatchNo , 'PICK' , { ... body , dispatchItemId : this . selectedItem . dispatchItemId } ) ; await this . recoverCommand ( ) } catch ( cause ) { this . setError ( cause , '拣货未提交' ) } } ,
async submitPick ( ) { if ( ! this . canPick || this . pendingCommand ) return ; try { const body = { recordVersion : requireDispatchVersion ( this . current . recordVersion ) , positionId : this . selectedReturnCarton ? null : this . selectedPosition . positionId , packages : this . selectedReturnCarton ? 1 : Number ( this . packages ) , productQuantity : this . selectedReturnCarton ? . productQuantity || 0 , cartonMark : this . selectedReturnCarton ? . cartonMark || null , remark : null } ; beginDispatchCommand ( this . current . dispatchNo , 'PICK' , { ... body , dispatchItemId : this . selectedItem . dispatchItemId } ) ; await this . recoverCommand ( ) } catch ( cause ) { this . setError ( cause , '拣货未提交' ) } } ,
async prepareLoading ( ) { if ( ! this . canComplete || this . pendingCommand ) return ; await this . run ( async ( ) => { const data = await getDispatchLoading ( this . current . dispatchNo ) ; if ( data . dispatchNo !== this . current . dispatchNo || data . pickingReconciled !== true || data . loadingComplete ? . allowed !== true ) throw new Error ( data . loadingComplete ? . blockReason || '装车前置未满足' ) ; requireDispatchVersion ( data . recordVersion ) ; this . loadingPreview = data ; this . loadingRows = data . pallets . map ( row => { const confirmed = data . confirmedLoad ? . manifestHash === data . manifestHash ? data . confirmedLoad . pallets . find ( item => String ( item . warehousePalletId ) === String ( row . warehousePalletId ) ) : null ; return { warehousePalletId : String ( row . warehousePalletId ) , palletNo : row . palletNo , palletSpaces : row . palletSpaces , deliveryIndex : row . deliveryIndex , weightKg : confirmed ? . weightKg ? ? ( row . evidence . some ( item => item . field === 'weightKg' && item . actual === true ) ? row . weightKg : null ) , volumeCbm : confirmed ? . volumeCbm ? ? ( row . evidence . some ( item => item . field === 'volumeCbm' && item . actual === true ) ? row . volumeCbm : null ) } } ) } ) } ,
async prepareLoading ( ) { if ( ! this . canComplete || this . pendingCommand ) return ; await this . run ( async ( ) => { const data = await getDispatchLoading ( this . current . dispatchNo ) ; if ( data . dispatchNo !== this . current . dispatchNo || data . pickingReconciled !== true || data . loadingComplete ? . allowed !== true ) throw new Error ( data . loadingComplete ? . blockReason || '装车前置未满足' ) ; requireDispatchVersion ( data . recordVersion ) ; this . loadingPreview = data ; this . loadingRows = data . pallets } ) } ,
setMeasurement ( row , field , event ) { if ( this . busy || this . pendingCommand ) return ; const value = event . detail . value === '' ? null : Number ( event . detail . value ) ; row [ field ] = value != null && Number . isFinite ( value ) ? value : null } ,
async completeLoading ( ) { if ( ! this . loadingPreview || this . pendingCommand || this . busy ) return ; try { if ( ! this . loadingRows . length || this . loadingRows . some ( row => row . deliveryIndex == null || ! Number . isInteger ( row . deliveryIndex ) || row . deliveryIndex < 1 ) ) throw new Error ( '托盘卸货站尚未匹配,请后台核对' ) ; beginDispatchCommand ( this . loadingPreview . dispatchNo , 'LOADING_COMPLETE' , { recordVersion : requireDispatchVersion ( this . loadingPreview . recordVersion ) , manifestHash : this . loadingPreview . manifestHash , remark : null } ) ; await this . recoverCommand ( ) } catch ( cause ) { this . setError ( cause , '装车完成未确认' ) } } ,
async completeLoading ( ) { if ( ! this . loadingPreview || this . pendingCommand || this . busy ) return ; try { if ( ! this . loadingRows . length || this . loadingRows . some ( row => row . weightKg == null || row . volumeCbm == null || row . palletSpaces == null || row . deliveryIndex == null || row . weightKg <= 0 || row . volumeCbm <= 0 || ! Number . isInteger ( row . palletSpaces ) || row . palletSpaces <= 0 || ! Number . isInteger ( row . deliveryIndex ) || row . deliveryIndex < 0 ) ) throw new Error ( '请核对各托真实重量、体积、托位及已匹配卸货站' ) ; beginDispatchCommand ( this . loadingPreview . dispatchNo , 'LOADING_COMPLETE' , { recordVersion : requireDispatchVersion ( this . loadingPreview . recordVersion ) , manifestHash : this . loadingPreview . manifestHash , measurements : this . loadingRows . map ( ( { warehousePalletId , palletSpaces , weightKg , volumeCbm , deliveryIndex } ) => ( { warehousePalletId , palletSpaces , weightKg , volumeCbm , deliveryIndex } ) ) , remark : null } ) ; await this . recoverCommand ( ) } catch ( cause ) { this . setError ( cause , '装车完成未确认' ) } } ,
updateReviewReason ( event ) { if ( this . busy || this . loading || this . pendingCommand ) return ; this . reviewReason = String ( event . detail . value || '' ) } ,
updateReviewReason ( event ) { if ( this . busy || this . loading || this . pendingCommand ) return ; this . reviewReason = String ( event . detail . value || '' ) } ,
async submitReview ( decision ) {
async submitReview ( decision ) {
if ( ! this . canReview || this . busy || this . loading || this . pendingCommand || ! [ 'APPROVED' , 'REJECTED' ] . includes ( decision ) ) return
if ( ! this . canReview || this . busy || this . loading || this . pendingCommand || ! [ 'APPROVED' , 'REJECTED' ] . includes ( decision ) ) return
if ( decision === 'APPROVED' && this . current . actions . LOADING _REVIEW ? . blockCode === 'APPROVAL_BLOCKED' ) return this . setError ( new Error ( this . current . actions . LOADING _REVIEW . blockReason ) , '预约及时间尚未核对' )
const reason = this . reviewReason . trim ( )
const reason = this . reviewReason . trim ( )
if ( decision === 'REJECTED' && ! reason ) return this . setError ( new Error ( '请填写驳回原因,退回客服处理' ) , '审核未提交' )
if ( decision === 'REJECTED' && ! reason ) return this . setError ( new Error ( '请填写驳回原因,退回客服处理' ) , '审核未提交' )
try {
try {
@@ -132,6 +153,7 @@ export default {
async run ( work , loading = false ) { if ( this . busy || this . loading ) return ; this . error = null ; this . successMessage = '' ; if ( loading ) this . loading = true ; else this . busy = true ; try { await work ( ) } catch ( cause ) { this . setError ( cause , '排车操作失败' ) } finally { this . loading = false ; this . busy = false } } ,
async run ( work , loading = false ) { if ( this . busy || this . loading ) return ; this . error = null ; this . successMessage = '' ; if ( loading ) this . loading = true ; else this . busy = true ; try { await work ( ) } catch ( cause ) { this . setError ( cause , '排车操作失败' ) } finally { this . loading = false ; this . busy = false } } ,
setError ( cause , fallback ) { this . error = toDispatchError ( cause , fallback ) ; playErrorSound ( ) } ,
setError ( cause , fallback ) { this . error = toDispatchError ( cause , fallback ) ; playErrorSound ( ) } ,
stageLabel ( task ) { if ( task . status === 'CANCELLED' ) return '已取消' ; if ( task . stage === 'BEFORE_LOADING' ) { if ( task . actions ? . LOADING _REVIEW ? . allowed === true ) return '待理货审核' ; if ( [ task . reviews ? . dispatchReview , task . reviews ? . loadingReview ] . some ( review => review ? . status === 'REJECTED' ) ) return '已退回客服处理' } ; return ( { BEFORE _LOADING : '装车前' , LOADING : '装车中' , AFTER _LOADING : '装车后' , DELIVERING : '派送中' , DELIVERED : '派送完成' } ) [ task . stage ] || '状态待核实' } ,
stageLabel ( task ) { if ( task . status === 'CANCELLED' ) return '已取消' ; if ( task . stage === 'BEFORE_LOADING' ) { if ( task . actions ? . LOADING _REVIEW ? . allowed === true ) return '待理货审核' ; if ( [ task . reviews ? . dispatchReview , task . reviews ? . loadingReview ] . some ( review => review ? . status === 'REJECTED' ) ) return '已退回客服处理' } ; return ( { BEFORE _LOADING : '装车前' , LOADING : '装车中' , AFTER _LOADING : '装车后' , DELIVERING : '派送中' , DELIVERED : '派送完成' } ) [ task . stage ] || '状态待核实' } ,
reviewTime ( value ) { if ( ! value ) return '未填写' ; const date = new Date ( new Date ( value ) . getTime ( ) + 8 * 60 * 60 * 1000 ) ; return Number . isNaN ( date . getTime ( ) ) ? '时间无效' : date . toISOString ( ) . slice ( 0 , 16 ) . replace ( 'T' , ' ' ) } ,
reviewLabel ( review ) { return ( { NOT _SUBMITTED : '未提交' , PENDING : '待审核' , APPROVED : '已通过' , REJECTED : '已驳回' } ) [ review ? . status ] || '待核对' } ,
reviewLabel ( review ) { return ( { NOT _SUBMITTED : '未提交' , PENDING : '待审核' , APPROVED : '已通过' , REJECTED : '已驳回' } ) [ review ? . status ] || '待核对' } ,
routeLabel ( item ) { return [ item . destination ? . warehouseCode , item . destination ? . warehouseName , item . destination ? . address ] . filter ( Boolean ) . join ( ' · ' ) || '目的仓未关联' } , itemProgress ( item ) { return item . scheduledPackages ? Math . min ( 100 , Math . round ( item . pickedPackages / item . scheduledPackages * 100 ) ) : 0 }
routeLabel ( item ) { return [ item . destination ? . warehouseCode , item . destination ? . warehouseName , item . destination ? . address ] . filter ( Boolean ) . join ( ' · ' ) || '目的仓未关联' } , itemProgress ( item ) { return item . scheduledPackages ? Math . min ( 100 , Math . round ( item . pickedPackages / item . scheduledPackages * 100 ) ) : 0 }
}
}
@@ -139,9 +161,30 @@ export default {
< / script >
< / script >
< style scoped >
< style scoped >
@ import './fulfillment.scss' ;
@ import './fulfillment.scss' ;
. stage - switch { position : sticky ; top : 0 ; z - index : 2 ; display : flex ; gap : 8 rpx ; margin - bottom : 20 rpx ; padding : 8 rpx ; border : 1 px solid # dbe5f0 ; border - radius : 20 rpx ; background : # eef2f7 ; }
. stage - tab { flex : 1 ; margin : 0 ; min - height : 84 rpx ; padding : 0 16 rpx ; border - radius : 14 rpx ; color : # 64748 b ; font - size : 28 rpx ; font - weight : 600 ; line - height : 84 rpx ; background : transparent ; }
. stage - tab : : after { border : 0 ; }
. stage - tab . active { color : # 1262 f4 ; background : # fff ; box - shadow : 0 1 px 4 px rgba ( 31 , 41 , 55 , .12 ) ; }
. stage - tab [ disabled ] { opacity : .55 ; }
. review - card textarea { box - sizing : border - box ; width : 100 % ; min - height : 160 rpx ; padding : 16 rpx ; border : 1 px solid # cbd5e1 ; border - radius : 8 rpx ; }
. review - card textarea { box - sizing : border - box ; width : 100 % ; min - height : 160 rpx ; padding : 16 rpx ; border : 1 px solid # cbd5e1 ; border - radius : 8 rpx ; }
. loading - overlay { position : fixed ; inset : 0 ; background : rgba ( 15 , 23 , 42 , .4 ) ; display : flex ; align - items : center ; justify - content : center ; z - index : 100 ; padding : 24 rpx ; }
. loading - overlay { position : fixed ; inset : 0 ; background : rgba ( 15 , 23 , 42 , .46 ) ; display : flex ; align - items : center ; justify - content : center ; z - index : 100 ; padding : 40 rpx ; box - sizing : border - box ; }
. loading - dialog { width : 100 % ; max - height : 90 % ; background : # fff ; border - radius : 16 rpx ; padding : 24 rpx ; display : flex ; flex - direction : column ; }
. loading - dialog { width : 100 % ; max - width : 620 rpx ; max - height : 88 vh ; overflow - y : auto ; box - sizing : border - box ; background : # fff ; border - radius : 28 rpx ; padding : 36 rpx 32 rpx 32 rpx ; box - shadow : 0 24 rpx 80 rpx rgba ( 15 , 23 , 42 , .18 ) ; }
. loading - dialog - body { max - height : 70 vh ; }
. loading - dialog - heading { display : flex ; flex - direction : column ; gap : 10 rpx ; }
. loading - dialog input { min - height : 72 rpx ; border : 1 px solid # cbd5e1 ; padding : 8 rpx ; margin : 8 rpx 0 ; }
. loading - dialog - title { color : # 142 c49 ; font - size : 36 rpx ; font - weight : 700 ; line - height : 1.4 ; }
. loading - dialog - number { color : # 7 a8aa0 ; font - size : 26 rpx ; line - height : 1.5 ; overflow - wrap : anywhere ; }
. loading - counts { display : flex ; margin : 28 rpx 0 ; padding : 24 rpx 0 ; border - radius : 20 rpx ; background : # f3f7ff ; }
. loading - count { flex : 1 ; display : flex ; align - items : center ; flex - direction : column ; gap : 8 rpx ; }
. loading - count + . loading - count { border - left : 1 px solid # dce6f6 ; }
. loading - count - value { color : # 1764 ec ; font - size : 48 rpx ; font - weight : 700 ; line - height : 1.2 ; }
. loading - count - unit { font - size : 26 rpx ; font - weight : 500 ; }
. loading - count - label { color : # 72839 b ; font - size : 24 rpx ; }
. loading - confirm - question { display : block ; color : # 253 a54 ; font - size : 30 rpx ; font - weight : 600 ; line - height : 1.5 ; }
. loading - confirm - note { display : block ; color : # 7 a8aa0 ; font - size : 25 rpx ; line - height : 1.5 ; margin - top : 10 rpx ; }
. loading - dialog - actions { display : flex ; gap : 20 rpx ; margin - top : 32 rpx ; }
. loading - dialog - actions button { flex : 1 ; min - width : 0 ; margin : 0 ; height : 88 rpx ; line - height : 88 rpx ; padding : 0 12 rpx ; border - radius : 16 rpx ; font - size : 30 rpx ; font - weight : 600 ; }
. loading - dialog - actions button : : after { border : 0 ; }
. loading - cancel { color : # 52657 d ; background : # f0f3f8 ; }
. loading - confirm { color : # fff ; background : # 1764 ec ; }
. loading - dialog - actions button [ disabled ] { opacity : .5 ; }
< / style >
< / style >