feat: gate PDA loading evidence by ready task

This commit is contained in:
2026-08-15 17:04:00 +08:00
parent 207c872dff
commit 3bb5da8ae7
6 changed files with 147 additions and 79 deletions
+92 -44
View File
@@ -14,18 +14,18 @@
<view class="work-content">
<view class="box-select-wrap">
<button class="box-select-btn" @tap="toggleBoxDropdown">
<text>{{ currentBox.boxNo }}<template v-if="currentBox.currentContainerNo"> · 当前柜号 {{ currentBox.currentContainerNo }}</template> · 订单 {{ currentBox.orderNo }}</text>
<text>集装箱号 {{ currentBox.currentContainerNo || '同步中' }} · 柜次号 {{ currentBox.cabinetNo || '同步中' }} · 订单 {{ currentBox.orderNo }}</text>
<text class="box-chevron" :class="{ open: boxDropdownOpen }">▼</text>
</button>
<view v-if="boxDropdownOpen" class="box-dropdown">
<view
v-for="(box, index) in boxes"
:key="box.boxNo"
:key="box.taskId || box.cabinetNo"
class="box-option"
:class="{ active: index === currentBoxIndex, completed: completedBoxes[index] }"
@tap="requestBoxSwitch(index)"
>
<view><text class="box-option-number">{{ box.boxNo }}</text><text class="box-option-order">{{ box.orderNo }}</text></view>
<view><text class="box-option-number">{{ box.currentContainerNo || '集装箱号同步中' }}</text><text class="box-option-order">柜次号 {{ box.cabinetNo || '同步中' }} · {{ box.orderNo }}</text></view>
<text v-if="completedBoxes[index]" class="box-option-state">已完成</text>
<text v-else-if="hasBoxDraft(index)" class="box-option-state draft">有草稿</text>
</view>
@@ -70,7 +70,7 @@
<text> / {{ summary.total }} 箱</text>
</template>
</view>
<button class="box-tasks-btn" @tap="openBoxList('pending')">箱任务 {{ completedCount }}/{{ boxes.length }}</button>
<button class="box-tasks-btn" @tap="openBoxList('pending')">柜次任务 {{ completedCount }}/{{ boxes.length }}</button>
</view>
</view>
<view class="progress-bar-bg">
@@ -81,7 +81,7 @@
<view class="pick-list">
<view v-if="orderedItems.length === 0" class="empty-state">
<text class="empty-state-icon">□</text>
<text class="empty-state-text">该箱号暂无拣货任务</text>
<text class="empty-state-text">当前集装箱暂无拣货明细</text>
</view>
<view
v-for="item in orderedItems"
@@ -297,7 +297,7 @@
<view class="complete-modal" role="dialog" aria-modal="true" aria-label="拣货完成" @tap.stop>
<PopupHeader title="拣货完成" @close="closeOverlay" />
<view class="complete-icon">✓</view>
<text class="complete-desc">箱号 {{ currentBox.boxNo }} · 订单 {{ currentBox.orderNo }}</text>
<text class="complete-desc">集装箱号 {{ currentBox.currentContainerNo }} · 柜次号 {{ currentBox.cabinetNo }} · 订单 {{ currentBox.orderNo }}</text>
<view class="complete-stats">
<view class="complete-stat">
<text class="complete-stat-num green">{{ completionSummary.picked }}</text>
@@ -324,10 +324,10 @@
</uni-popup>
<uni-popup ref="allCompletePopup" type="center" :is-mask-click="true" @change="handleOverlayChange('allComplete', $event)">
<view class="complete-modal" role="dialog" aria-modal="true" aria-label="全部箱号已完成" @tap.stop>
<PopupHeader title="全部箱号已完成" @close="closeOverlay" />
<view class="complete-modal" role="dialog" aria-modal="true" aria-label="全部柜次已完成" @tap.stop>
<PopupHeader title="全部柜次已完成" @close="closeOverlay" />
<view class="complete-icon">✓</view>
<text class="complete-desc">本班次演示任务 {{ boxes.length }} 箱已全部处理</text>
<text class="complete-desc">本班次 {{ boxes.length }} 个柜次任务已全部处理</text>
<button class="all-complete-btn" @tap="leavePicking">返回工作台</button>
</view>
</uni-popup>
@@ -385,7 +385,7 @@
selectedIncompleteReason: '',
completionSummary: { picked: 0, partial: 0, skipped: 0, autoSkipped: 0 },
evidenceSlots: EVIDENCE_SLOTS,
evidenceFiles: {}, evidenceDrafts: {}, evidencePreviewCache: {}, evidenceQueueItems: [], evidenceQueue: null, evidenceContextId: 0, evidenceIdentity: null, cameraPicking: false, sealNo: '', loadingVersion: null, pickingVersion: null, remotePick: null,
evidenceFiles: {}, evidenceDrafts: {}, evidencePreviewCache: {}, evidenceQueueItems: [], evidenceQueue: null, evidenceContextId: 0, evidenceIdentity: null, cameraPicking: false, evidencePreviewOpening: false, evidencePreviewReturning: false, sealNo: '', loadingVersion: null, pickingVersion: null, remotePick: null,
remoteTaskId: null, assignmentVersion: null, submitId: '',
taskRequestId: 0,
remoteError: '', remoteLoading: false, submitting: false,
@@ -424,6 +424,11 @@
return Boolean(this.completedBoxes[this.currentBoxIndex])
|| isPickingCompleteTask(this.remotePick)
},
pickingOperationReady() {
return this.remotePick?.workflowStatus === 'IN_PROGRESS'
&& Boolean(this.remoteTaskId && this.currentBox.inboundId && this.currentBox.loadingPlanId && this.currentBox.containerBookingId)
&& this.pickingVersion !== null
},
loadingStageReady() {
return this.pickingCompleted
&& this.evidenceIdentity
@@ -461,10 +466,10 @@
return ''
},
submitButtonText() {
return this.completedBoxes[this.currentBoxIndex] || this.pickingCompleted ? '该箱已完成' : this.summary.buttonText
return this.completedBoxes[this.currentBoxIndex] || this.pickingCompleted ? '该柜次已完成' : this.summary.buttonText
},
submitStatusHint() {
if (this.completedBoxes[this.currentBoxIndex] || this.pickingCompleted) return '本箱任务已完成'
if (this.completedBoxes[this.currentBoxIndex] || this.pickingCompleted) return '本柜次任务已完成'
if (!this.summary.canSubmit) return '请先处理至少一件'
if (this.summary.pending > 0) return '提交时需选择原因'
return `已处理 ${this.summary.processed}/${this.summary.total}`
@@ -494,10 +499,10 @@
},
onLoad(options = {}) {
if (!requireSession()) return
if (options.taskId && (options.cabinetNo || options.containerNo)) {
if (options.taskId) {
this.boxes = [{
boxNo: String(options.cabinetNo || options.containerNo), cabinetNo: String(options.cabinetNo || options.containerNo), currentContainerNo: options.currentContainerNo ? String(options.currentContainerNo) : null, orderNo: String(options.orderNo || options.inboundCode || ''),
taskId: String(options.taskId), loadingPlanId: options.loadingPlanId ? String(options.loadingPlanId) : null, containerBookingId: options.containerBookingId ? String(options.containerBookingId) : null,
boxNo: String(options.currentContainerNo || options.cabinetNo || '同步中'), cabinetNo: options.cabinetNo ? String(options.cabinetNo) : null, currentContainerNo: options.currentContainerNo ? String(options.currentContainerNo) : null, orderNo: String(options.orderNo || options.inboundCode || ''),
taskId: String(options.taskId), loadingPlanId: null, containerBookingId: null,
inboundId: options.inboundId ? String(options.inboundId) : null,
inboundVersion: options.inboundVersion == null ? null : Number(options.inboundVersion),
warehouseId: options.warehouseId ? String(options.warehouseId) : null,
@@ -507,16 +512,27 @@
this.remoteTaskId = options.taskId ? String(options.taskId) : null
}
},
onShow() {
if (!requireSession()) return
this.startHardwareScanner()
if (this.cameraPicking) {
this.debugEvidence('choose.resume', { evidenceIdentity: this.evidenceIdentity })
return
}
this.syncRemoteTask()
},
onHide() { this.stopHardwareScanner() },
onShow() {
if (!requireSession()) return
this.startHardwareScanner()
if (this.cameraPicking) {
this.debugEvidence('choose.resume', { evidenceIdentity: this.evidenceIdentity })
return
}
if (this.evidencePreviewReturning) {
this.evidencePreviewReturning = false
this.debugEvidence('preview.resume', { evidenceIdentity: this.evidenceIdentity })
return
}
this.syncRemoteTask()
},
onHide() {
if (this.evidencePreviewOpening) {
this.evidencePreviewOpening = false
this.evidencePreviewReturning = true
}
this.stopHardwareScanner()
},
onUnload() { this.stopHardwareScanner(); this.persistSession(); this.clearTimers() },
beforeUnmount() { this.stopHardwareScanner(); this.clearTimers() },
onBackPress() {
@@ -593,6 +609,11 @@
}
return true
},
requirePickingOperation() {
if (this.pickingOperationReady) return true
this.showToast('任务尚未领取完成,请稍候或刷新重试', 'warning')
return false
},
requireEvidenceStage() {
if (!this.pickingCompleted) {
this.showToast('请先提交拣货完成', 'warning')
@@ -680,7 +701,7 @@
savePickingSession({ boxes: this.boxes, currentBoxIndex: this.currentBoxIndex, itemStates: this.itemStates, partialQuantities: this.partialQuantities, scannedBoxCodes: this.scannedBoxCodes, boxDrafts: this.boxDrafts, completedBoxes: this.completedBoxes, scanValue: this.scanValue, scanHistory: this.scanHistory }, this.draftIdentity())
},
leavePicking() { this.persistSession(); this.activeOverlay = ''; navigateBackOr('/pages/home/home') },
clearTimers() { clearTimeout(this._toastTimer) },
clearTimers() { clearTimeout(this._toastTimer); clearTimeout(this._evidencePreviewOpenTimer) },
scrollToTop() { this.$nextTick(() => { if (typeof uni !== 'undefined' && typeof uni.pageScrollTo === 'function') uni.pageScrollTo({ scrollTop: 0, duration: 0 }) }) },
saveCurrentBoxDraft() {
if (!this.currentBox.boxNo) return
@@ -739,7 +760,7 @@
this.activeStage = 'picking'
this.currentBox.inboundId = null
this.currentBox.inboundVersion = null
this.remoteTaskId = null
this.remoteTaskId = this.currentBox.taskId || null
this.remotePick = null
this.pickingVersion = null
this.loadingVersion = null
@@ -748,8 +769,8 @@
this.boxDropdownOpen = false
this.loadBoxState(index)
this.scrollToTop()
if (session.token && session.tenantId && this.currentBox.boxNo) this.syncRemoteTask()
this.showToast(`切换至 ${this.currentBox.boxNo}`, 'success')
if (session.token && this.currentBox.taskId) this.syncRemoteTask()
this.showToast(`切换至集装箱 ${this.currentBox.currentContainerNo || '同步中'}`, 'success')
},
itemState(item) {
return this.itemStates[item.id] || ITEM_STATUS.PENDING
@@ -765,6 +786,7 @@
return `${item.desc}${reason}`
},
processScannedValue(scanValue, source = 'broadcast') {
if (!this.requirePickingOperation()) return
const value = scanValue.trim()
if (!value) {
const manual = source === 'manual'
@@ -812,7 +834,10 @@
if (!current || !urls.length) { this.showToast('照片地址尚未同步,请刷新后重试', 'warning'); return }
if (typeof uni.previewImage !== 'function') { this.showToast('当前环境不支持预览照片', 'warning'); return }
this.debugEvidence('preview.open', { evidenceType: type, evidenceIdentity: this.evidenceIdentity, count: urls.length })
uni.previewImage({ current, urls })
this.evidencePreviewOpening = true
clearTimeout(this._evidencePreviewOpenTimer)
this._evidencePreviewOpenTimer = setTimeout(() => { this.evidencePreviewOpening = false }, 1500)
uni.previewImage({ current, urls, fail: () => { this.evidencePreviewOpening = false } })
},
evidenceItems(result) {
if (Array.isArray(result?.evidences)) return result.evidences
@@ -835,25 +860,33 @@
return ''
},
async syncRemoteTask() {
if (!isRemoteReady(this.currentBox.inboundId || this.currentBox.boxNo)) return
if (!session.token) return
const requestedTaskId = this.remoteTaskId || this.currentBox.taskId
if (!requestedTaskId) { this.remoteError = '当前没有可进入的拣货任务,请返回任务列表刷新'; return }
this.rememberEvidenceDraft()
const requestId = ++this.taskRequestId
const selector = { taskId: this.remoteTaskId || this.currentBox.taskId, cabinetNo: this.currentBox.cabinetNo, loadingPlanId: this.currentBox.loadingPlanId }
const selector = { taskId: requestedTaskId }
this.currentBox.inboundId = null
this.currentBox.loadingPlanId = null
this.currentBox.containerBookingId = null
this.loadingVersion = null
this.resetEvidenceState()
this.debugSubmit('refresh.start', { requestId, cabinetNo: this.currentBox.cabinetNo || null, selector })
this.debugSubmit('refresh.start', { requestId, selector })
this.remoteLoading = true
this.remoteError = ''
try {
const warehouseId = this.currentBox.warehouseId || session.currentUser?.warehouseId || session.currentUser?.warehouse
const task = await getContainerPickingTask(selector, warehouseId)
if (requestId !== this.taskRequestId) return
if (!task) throw new Error('未找到当前柜次的拣货任务')
if (!task) throw new Error('服务端未返回当前拣货任务')
const detailTaskId = task.taskId ?? task.id
if (String(detailTaskId || '') !== String(requestedTaskId)) throw new Error('服务端返回的拣货任务与当前选择不一致,请返回刷新')
if (!task.inboundId || !task.loadingPlanId || !task.containerBookingId) throw new Error('任务尚未同步完整作业关联,请返回任务列表刷新')
if (task.version === null || task.version === undefined) throw new Error('任务状态尚未同步,请刷新重试')
if (!task.currentContainerNo) throw new Error('任务尚未同步集装箱号,请返回任务列表刷新')
const remoteItems = task.items || task.pickItems || task.details || task.lines
if (Array.isArray(remoteItems) && remoteItems.length) {
if (!Array.isArray(remoteItems) || !remoteItems.length) throw new Error('当前拣货任务暂无可作业明细,请返回刷新')
{
this.currentBox.items = remoteItems.map((item) => ({
...item,
id: item.id ?? item.itemId,
@@ -873,10 +906,11 @@
skipReason: item.skipReason || '', finalState: item.finalState || '', finalPartialQuantity: item.finalPartialQuantity || 0
}))
const firstItem = this.currentBox.items[0]
const evidenceItem = this.currentBox.items.find((item) => String(item.inboundId || '') === String(task.inboundId))
if (!evidenceItem) throw new Error('任务明细与入库单关联不一致,请返回刷新')
this.currentBox.orderNo = firstItem.orderNo || firstItem.inboundCode || this.currentBox.orderNo
if (!firstItem.inboundId) throw new Error('服务端任务明细缺少 inboundId,无法进入装柜取证')
this.currentBox.inboundId = firstItem.inboundId
this.currentBox.tenantId = firstItem.tenantId || null
this.currentBox.inboundId = task.inboundId
this.currentBox.tenantId = evidenceItem.tenantId || null
const serverStates = createItemStates(this.currentBox.items)
const serverPartialQuantities = {}
this.currentBox.items.forEach((item) => {
@@ -890,28 +924,35 @@
this.partialQuantities = serverPartialQuantities
this.scannedBoxCodes = {}
}
this.remoteTaskId = task.taskId ?? task.id
this.remoteTaskId = detailTaskId
if (!this.remoteTaskId) throw new Error('服务端任务缺少 taskId,无法开始拣货')
this.assignmentVersion = task.assignmentVersion ?? task.version ?? 0
this.currentBox.loadingPlanId = task.loadingPlanId
this.currentBox.containerBookingId = task.containerBookingId
this.currentBox.cabinetNo = task.cabinetNo
this.currentBox.boxNo = task.cabinetNo
this.currentBox.boxNo = task.currentContainerNo
this.currentBox.currentContainerNo = task.currentContainerNo
const remoteTask = normalizePickingTask(task)
this.assignmentVersion = remoteTask.assignment?.version ?? remoteTask.assignmentVersion ?? (remoteTask.workflowStatus === 'UNASSIGNED' ? 0 : null)
if (this.assignmentVersion === null) throw new Error('任务领取状态尚未同步,请刷新重试')
let claimed = null
if (!isPickingCompleteTask(remoteTask)) {
if (remoteTask.workflowStatus === 'UNASSIGNED') {
const operationId = `claim-${session.currentUser?.id || 'operator'}-${this.remoteTaskId}-${task.loadingPlanId}-${task.containerBookingId}`
claimed = await claimContainerPickingTask(this.remoteTaskId, this.assignmentVersion, operationId)
if (requestId !== this.taskRequestId) return
} else if (remoteTask.workflowStatus === 'IN_PROGRESS') {
const assigneeId = remoteTask.assignment?.assigneeId
if (!assigneeId || String(assigneeId) !== String(session.currentUser?.id || '')) throw new Error('当前任务已由其他作业员领取,请返回任务列表刷新')
} else if (!isPickingCompleteTask(remoteTask)) {
throw new Error('当前任务尚未进入可拣货状态,请返回任务列表刷新')
}
this.remotePick = normalizePickingTask({ ...task, ...(claimed || {}) })
if (remoteTask.workflowStatus === 'UNASSIGNED' && this.remotePick.workflowStatus !== 'IN_PROGRESS') throw new Error('任务领取尚未完成,请刷新重试')
this.pickingVersion = this.remotePick.pickingVersion ?? this.remotePick.version ?? null
this.loadingVersion = task.inboundVersion ?? task.loadingVersion ?? this.remotePick.inboundVersion ?? this.remotePick.loadingVersion ?? null
this.loadingVersion = this.remotePick.inboundVersion ?? this.remotePick.loadingVersion ?? null
this.evidenceIdentity = this.captureEvidenceIdentity()
this.restoreEvidenceDraft()
this.debugSubmit('refresh.ready', { requestId, workflowStatus: this.remotePick.workflowStatus, taskStatus: this.remotePick.taskStatus, assignmentVersion: this.assignmentVersion, loadingVersion: this.loadingVersion, evidenceIdentity: this.evidenceIdentity })
this.assignmentVersion = this.remotePick.assignmentVersion ?? this.assignmentVersion
this.assignmentVersion = this.remotePick.assignment?.version ?? this.remotePick.assignmentVersion ?? this.assignmentVersion
if (!this.pickingCompleted) this.restoreRemoteDraft()
if (this.loadingStageReady) await this.loadLoadingEvidence()
} catch (error) {
@@ -1133,6 +1174,7 @@
this.persistSession()
},
pickItem(id, source = 'manual') {
if (!this.requirePickingOperation()) return
const item = this.currentBox.items.find((entry) => entry.id === id)
const available = this.availablePickQuantity(item)
if (available !== null && available < Number(item.qty || 0)) { this.showToast(`库存/库位可用量不足:可拣 ${available}`, 'error'); return }
@@ -1146,6 +1188,7 @@
this.scanHistory = [entry, ...this.scanHistory].slice(0, 3)
},
undoItem(id) {
if (!this.requirePickingOperation()) return
const item = this.currentBox.items.find((entry) => entry.id === id)
if (item) item.skipReason = ''
this.itemStates = { ...this.itemStates, [id]: ITEM_STATUS.PENDING }
@@ -1158,11 +1201,13 @@
this.persistSession()
},
openSkip(id) {
if (!this.requirePickingOperation()) return
this.skipTargetId = id
this.selectedSkipReason = ''
this.openOverlay('skip')
},
confirmSkip() {
if (!this.requirePickingOperation()) return
if (!this.selectedSkipReason) {
this.showToast('请先选择跳拣原因', 'warning')
return
@@ -1176,6 +1221,7 @@
this.persistSession()
},
openPartial(id) {
if (!this.requirePickingOperation()) return
this.partialTargetId = id
this.partialInput = ''
this.openOverlay('partial')
@@ -1191,6 +1237,7 @@
})
},
confirmPartial() {
if (!this.requirePickingOperation()) return
const item = this.partialTargetItem
if (!item) return
if (!this.partialInputValid) return
@@ -1232,6 +1279,7 @@
this.requestBoxSwitch(entry.index)
},
submitPicking() {
if (!this.requirePickingOperation()) return
if (!this.summary.canSubmit || this.completedBoxes[this.currentBoxIndex] || this.pickingCompleted) return
if (this.summary.pending > 0) {
this.selectedIncompleteReason = ''