fix(pda): isolate loading evidence by cabinet context

This commit is contained in:
2026-08-14 20:58:38 +08:00
parent a5eca66bfd
commit 455e81fbbd
3 changed files with 222 additions and 36 deletions
+139 -35
View File
@@ -117,7 +117,7 @@
</view>
</view>
<view class="loading-evidence-section" v-if="currentBox.inboundId">
<view v-if="currentBox.inboundId && pickingCompleted" class="loading-evidence-section">
<view class="section-heading-row">
<view><text class="section-title">装柜证据</text><text class="section-subtitle">空仓、半仓、满仓、锁仓及带锁码照</text></view>
<text class="sync-state" :class="remoteSyncState.tone">{{ remoteSyncState.label }}</text>
@@ -141,8 +141,11 @@
<button class="evidence-btn seal-btn" @tap="saveSealNo">保存封签</button>
</view>
<view v-if="remoteError" class="remote-error-row"><text class="remote-error" role="alert">{{ remoteError }}</text><button class="evidence-retry-btn" :disabled="remoteLoading" @tap="refreshTask">刷新任务</button></view>
<button class="loading-complete-btn" :disabled="submitting || loadingVersion === null || !completedBoxes[currentBoxIndex] || !evidenceComplete" @tap="completeLoadingTask">确认装柜完成</button>
<text v-if="loadingVersion === null" class="version-hint">等待统一订单详情提供装柜版本号后可提交完成</text>
<button class="loading-complete-btn" :disabled="submitting || loadingVersion === null || !evidenceComplete" @tap="completeLoadingTask">确认装柜完成</button>
<view v-if="loadingVersion === null" class="remote-error-row">
<text class="version-hint" role="status">状态尚未同步,请刷新重试</text>
<button class="evidence-retry-btn" :disabled="remoteLoading" @tap="refreshTask">刷新任务</button>
</view>
</view>
<view class="submit-section">
@@ -332,6 +335,16 @@
import ScanTargetInput from '@/components/ScanTargetInput.vue'
import PopupHeader from '@/components/PopupHeader.vue'
const VERSION_SYNC_MESSAGE = '状态尚未同步,请刷新重试'
function isPickingCompleteTask(task) {
const workflowStatus = task?.workflowStatus
const taskStatus = task?.taskStatus ?? task?.status
return workflowStatus === 'AWAITING_REVIEW'
|| workflowStatus === 'COMPLETED'
|| taskStatus === 'COMPLETED'
}
export default {
components: { PopupHeader, ScanTargetInput },
data() {
@@ -354,8 +367,9 @@
selectedIncompleteReason: '',
completionSummary: { picked: 0, partial: 0, skipped: 0, autoSkipped: 0 },
evidenceSlots: EVIDENCE_SLOTS,
evidenceFiles: {}, evidenceQueueItems: [], evidenceQueue: null, sealNo: '', loadingVersion: null, pickingVersion: null, remotePick: null,
evidenceFiles: {}, evidenceQueueItems: [], evidenceQueue: null, evidenceContextId: 0, sealNo: '', loadingVersion: null, pickingVersion: null, remotePick: null,
remoteTaskId: null, assignmentVersion: null, submitId: '',
taskRequestId: 0,
remoteError: '', remoteLoading: false, submitting: false,
toast: { visible: false, message: '', type: '' }
}
@@ -388,6 +402,10 @@
pendingBoxCount() {
return this.boxes.length - this.completedCount
},
pickingCompleted() {
return Boolean(this.completedBoxes[this.currentBoxIndex])
|| isPickingCompleteTask(this.remotePick)
},
partialTargetItem() {
return this.currentBox.items.find((item) => item.id === this.partialTargetId) || null
},
@@ -446,10 +464,7 @@
}
},
created() {
this.evidenceQueue = createEvidenceUploadQueue({
upload: (item, onProgress) => uploadEvidenceFile(item.inboundId, item.evidenceType, item.localPath, item.tenantId, onProgress),
onChange: (items) => { this.evidenceQueueItems = items }
})
this.resetEvidenceState()
},
onLoad(options = {}) {
if (!requireSession()) return
@@ -481,6 +496,26 @@
return false
},
methods: {
isEvidenceContextActive(contextId, inboundId) {
return contextId === this.evidenceContextId
&& String(this.currentBox.inboundId ?? '') === String(inboundId ?? '')
},
requireEvidenceStage() {
if (this.pickingCompleted) return true
this.showToast('请先提交拣货完成', 'warning')
return false
},
resetEvidenceState() {
const contextId = this.evidenceContextId + 1
this.evidenceContextId = contextId
this.evidenceFiles = {}
this.evidenceQueueItems = []
this.sealNo = ''
this.evidenceQueue = createEvidenceUploadQueue({
upload: (item, onProgress) => uploadEvidenceFile(item.inboundId, item.evidenceType, item.localPath, item.tenantId, onProgress),
onChange: (items) => { if (this.evidenceContextId === contextId) this.evidenceQueueItems = items }
})
},
normalizeScanHistory(entries) {
if (!Array.isArray(entries)) return []
return entries.map((entry) => {
@@ -598,11 +633,15 @@
},
selectBox(index) {
this.saveCurrentBoxDraft()
this.taskRequestId += 1
this.submitting = false
this.currentBoxIndex = index
this.remoteTaskId = null
this.remotePick = null
this.pickingVersion = null
this.loadingVersion = null
this.assignmentVersion = null
this.resetEvidenceState()
this.boxDropdownOpen = false
this.loadBoxState(index)
this.scrollToTop()
@@ -670,11 +709,13 @@
},
async syncRemoteTask() {
if (!isRemoteReady(this.currentBox.inboundId || this.currentBox.boxNo)) return
const requestId = ++this.taskRequestId
this.remoteLoading = true
this.remoteError = ''
try {
const warehouseId = this.currentBox.warehouseId || session.currentUser?.warehouseId || session.currentUser?.warehouse
const task = await getContainerPickingTask({ taskId: this.remoteTaskId || this.currentBox.taskId, cabinetNo: this.currentBox.cabinetNo, loadingPlanId: this.currentBox.loadingPlanId, containerNo: this.currentBox.currentContainerNo }, warehouseId)
const task = await getContainerPickingTask({ taskId: this.remoteTaskId || this.currentBox.taskId, cabinetNo: this.currentBox.cabinetNo, loadingPlanId: this.currentBox.loadingPlanId }, warehouseId)
if (requestId !== this.taskRequestId) return
if (!task) throw new Error('未找到当前柜次的拣货任务')
const remoteItems = task.items || task.pickItems || task.details || task.lines
if (Array.isArray(remoteItems) && remoteItems.length) {
@@ -710,22 +751,38 @@
this.currentBox.cabinetNo = task.cabinetNo
this.currentBox.boxNo = task.cabinetNo
this.currentBox.currentContainerNo = task.currentContainerNo
const operationId = `claim-${session.currentUser?.id || 'operator'}-${this.remoteTaskId}-${task.loadingPlanId}-${task.containerBookingId}`
const claimed = await claimContainerPickingTask(this.remoteTaskId, this.assignmentVersion, operationId)
const remoteTask = normalizePickingTask(task)
let claimed = null
if (!isPickingCompleteTask(remoteTask)) {
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
}
this.remotePick = normalizePickingTask({ ...task, ...(claimed || {}) })
this.pickingVersion = this.remotePick.pickingVersion ?? this.remotePick.version ?? null
this.loadingVersion = this.currentBox.inboundVersion ?? this.remotePick.inboundVersion ?? null
this.assignmentVersion = this.remotePick.assignmentVersion ?? this.assignmentVersion
this.restoreRemoteDraft()
if (this.currentBox.inboundId) {
const evidence = await getEvidence(this.currentBox.inboundId, this.currentBox.tenantId)
this.evidenceFiles = (evidence && evidence.evidences ? evidence.evidences : []).reduce((all, item) => { (all[item.evidenceType] ||= []).push(item); return all }, {})
const seal = this.evidenceEntry('SEAL')
if (seal) this.sealNo = seal.sealNo || ''
}
this.resetEvidenceState()
if (this.pickingCompleted) await this.loadLoadingEvidence()
} catch (error) {
if (requestId !== this.taskRequestId) return
this.remoteError = error.network ? '网络不可用,无法开始拣货,恢复网络后请重试' : (error.message || '任务同步失败')
} finally { this.remoteLoading = false }
} finally { if (requestId === this.taskRequestId) this.remoteLoading = false }
},
async loadLoadingEvidence() {
if (!this.pickingCompleted || !this.currentBox.inboundId || !isRemoteReady(this.currentBox.inboundId)) return
const contextId = this.evidenceContextId
try {
const evidence = await getEvidence(this.currentBox.inboundId, this.currentBox.tenantId)
if (contextId !== this.evidenceContextId) return
this.evidenceFiles = (evidence && evidence.evidences ? evidence.evidences : []).reduce((all, item) => { (all[item.evidenceType] ||= []).push(item); return all }, {})
const seal = this.evidenceEntry('SEAL')
this.sealNo = seal?.sealNo || ''
} catch (error) {
if (contextId !== this.evidenceContextId) return
this.remoteError = error.network ? '网络失败,装柜证据未加载,请刷新重试' : (error.message || '装柜证据加载失败')
}
},
async refreshTask() {
if (!this.currentBox.boxNo) return
@@ -739,73 +796,118 @@
return this.evidenceEntry(type) ? '继续拍照' : '拍照'
},
chooseEvidence(slot) {
if (!this.requireEvidenceStage()) return
if (!isOnline()) { this.showToast('当前网络不可用,请联网后再拍照上传', 'warning'); return }
if (typeof uni.chooseImage !== 'function') { this.showToast('当前环境不支持选择照片', 'warning'); return }
const contextId = this.evidenceContextId
const inboundId = this.currentBox.inboundId
uni.chooseImage({ count: 1, sizeType: ['compressed'], sourceType: ['camera'], success: (result) => {
if (!this.isEvidenceContextActive(contextId, inboundId)) return
const path = result.tempFilePaths && result.tempFilePaths[0]
if (!path) return
const next = { evidenceType: slot.type, fileName: path.split('/').pop() || path, localPath: path, uploadStatus: 'PENDING' }
this.evidenceFiles = { ...this.evidenceFiles, [slot.type]: [...(this.evidenceFiles[slot.type] || []), next] }
this.queueEvidenceUpload(slot.type, path)
}, fail: () => this.showToast('未选择照片,可稍后重试', 'warning') })
}, fail: () => {
if (!this.isEvidenceContextActive(contextId, inboundId)) return
this.showToast('未选择照片,可稍后重试', 'warning')
} })
},
async queueEvidenceUpload(type, path) {
if (!this.requireEvidenceStage()) return
if (!this.currentBox.inboundId || !isRemoteReady(this.currentBox.inboundId)) { this.showToast('当前会话未连接服务端,证据保留本地', 'warning'); return }
const entry = this.evidenceQueue.add({ inboundId: this.currentBox.inboundId, tenantId: this.currentBox.tenantId, evidenceType: type, localPath: path })
await this.evidenceQueue.flush()
const latest = this.evidenceQueue.snapshot().find((item) => item.id === entry.id)
const contextId = this.evidenceContextId
const queue = this.evidenceQueue
const entry = queue.add({ inboundId: this.currentBox.inboundId, tenantId: this.currentBox.tenantId, evidenceType: type, localPath: path })
await queue.flush()
if (contextId !== this.evidenceContextId || queue !== this.evidenceQueue) return
const latest = queue.snapshot().find((item) => item.id === entry.id)
if (latest?.status !== 'SUCCEEDED') { this.remoteError = latest?.error || '文件上传失败'; return }
const current = this.evidenceEntry(type) || {}
this.evidenceFiles = { ...this.evidenceFiles, [type]: [...(this.evidenceFiles[type] || []).slice(0, -1), { ...current, fileId: latest.result?.fileId, fileName: latest.result?.fileName || current.fileName, uploadStatus: 'SUCCEEDED' }] }
await this.saveEvidence(type)
},
async retryEvidence(type) {
if (!this.requireEvidenceStage()) return
const item = this.evidenceQueueEntry(type)
if (!item || !this.evidenceQueue.retry(item.id)) return
const contextId = this.evidenceContextId
const queue = this.evidenceQueue
if (!item || !queue.retry(item.id)) return
this.remoteError = ''
await this.evidenceQueue.flush()
const latest = this.evidenceQueue.latest(type)
await queue.flush()
if (contextId !== this.evidenceContextId || queue !== this.evidenceQueue) return
const latest = queue.latest(type)
if (latest?.status !== 'SUCCEEDED') { this.remoteError = latest?.error || '文件上传失败'; return }
const current = this.evidenceEntry(type) || {}
this.evidenceFiles = { ...this.evidenceFiles, [type]: [...(this.evidenceFiles[type] || []).slice(0, -1), { ...current, fileId: latest.result?.fileId, fileName: latest.result?.fileName || current.fileName, uploadStatus: 'SUCCEEDED' }] }
await this.saveEvidence(type)
},
async saveEvidence(type, operationId = null) {
if (!this.requireEvidenceStage()) return
if (!this.currentBox.inboundId) { this.showToast('演示任务已记录本地证据', 'success'); return }
if (!isRemoteReady(this.currentBox.inboundId)) { this.showToast('当前会话未连接服务端,证据保留本地', 'warning'); return }
const contextId = this.evidenceContextId
const inboundId = this.currentBox.inboundId
const tenantId = this.currentBox.tenantId
const current = this.evidenceEntry(type) || {}
if (!current.fileId) { this.showToast('照片尚未上传完成', 'warning'); return }
const payload = { expectedVersion: current.version || 0, evidenceType: type, fileId: current.fileId || null, fileName: current.fileName || null, sealNo: type === 'SEAL' ? this.sealNo : null, remark: null, operationId: operationId || `evidence-${this.remoteTaskId || this.currentBox.inboundId}-${type}-${current.fileId || 'seal'}` }
try {
const result = await upsertEvidence(this.currentBox.inboundId, payload, this.currentBox.tenantId)
const result = await upsertEvidence(inboundId, payload, tenantId)
if (!this.isEvidenceContextActive(contextId, inboundId)) return
this.evidenceFiles = (result && result.evidences ? result.evidences : []).reduce((all, item) => { const list = (all[item.evidenceType] || []).filter((entry) => entry.id || entry.fileId !== item.fileId); const index = list.findIndex((entry) => entry.id === item.id); all[item.evidenceType] = index >= 0 ? list.map((entry, i) => i === index ? item : entry) : [...list, item]; return all }, this.evidenceFiles)
this.showToast(`${type === 'SEAL' ? '封签' : '照片'}已保存`, 'success')
} catch (error) { this.remoteError = error.network ? '网络失败,证据未提交,请重试' : (error.message || '证据保存失败'); this.showToast('证据保存失败', 'error') }
} catch (error) {
if (!this.isEvidenceContextActive(contextId, inboundId)) return
this.remoteError = error.network ? '网络失败,证据未提交,请重试' : (error.message || '证据保存失败')
this.showToast('证据保存失败', 'error')
}
},
async saveSealNo() {
if (!this.requireEvidenceStage()) return
if (!String(this.sealNo || '').trim()) { this.showToast('请输入封签号', 'warning'); return }
await this.saveEvidence('SEAL', `seal-no-${this.remoteTaskId || this.currentBox.inboundId}-${Date.now()}`)
},
async voidEvidencePhoto(photo) {
if (!this.requireEvidenceStage()) return
if (!photo?.id || !this.currentBox.inboundId) return
const contextId = this.evidenceContextId
const inboundId = this.currentBox.inboundId
const tenantId = this.currentBox.tenantId
const reason = 'PDA现场标记错图'
try {
await voidEvidence(this.currentBox.inboundId, photo.id, { expectedVersion: photo.version || 0, reason, operationId: `void-${this.currentBox.inboundId}-${photo.id}-${photo.version || 0}` }, this.currentBox.tenantId)
await voidEvidence(inboundId, photo.id, { expectedVersion: photo.version || 0, reason, operationId: `void-${inboundId}-${photo.id}-${photo.version || 0}` }, tenantId)
if (!this.isEvidenceContextActive(contextId, inboundId)) return
this.evidenceFiles = { ...this.evidenceFiles, [photo.evidenceType]: (this.evidenceFiles[photo.evidenceType] || []).map((item) => item.id === photo.id ? { ...item, status: 'VOID', voidReason: reason } : item) }
this.showToast('照片已作废,可重新拍摄', 'success')
} catch (error) {
if (!this.isEvidenceContextActive(contextId, inboundId)) return
this.remoteError = error.network ? '网络失败,照片未作废,请重试' : (error.message || '照片作废失败')
this.showToast('照片作废失败', 'error')
}
},
async completeLoadingTask() {
if (!isRemoteReady(this.currentBox.inboundId) || this.loadingVersion === null) return
if (!this.completedBoxes[this.currentBoxIndex]) { this.showToast('请先提交拣货完成', 'warning'); return }
if (!this.requireEvidenceStage()) return
if (!isRemoteReady(this.currentBox.inboundId)) { this.showToast('装柜任务尚未同步,请刷新后重试', 'warning'); return }
if (this.loadingVersion === null) { this.showToast(VERSION_SYNC_MESSAGE, 'warning'); return }
if (!this.evidenceComplete) { this.showToast('五类有效照片和封签号均为必填', 'warning'); return }
const contextId = this.evidenceContextId
const inboundId = this.currentBox.inboundId
const tenantId = this.currentBox.tenantId
const loadingVersion = this.loadingVersion
this.submitting = true
try { await completeLoading(this.currentBox.inboundId, { expectedVersion: this.loadingVersion, remark: 'PDA确认装柜完成' }, this.currentBox.tenantId); this.showToast('已确认装柜完成', 'success') }
catch (error) { this.remoteError = error.network ? '网络失败,装柜完成未提交' : (error.message || '装柜完成失败'); this.showToast('装柜完成失败', 'error') }
finally { this.submitting = false }
try {
await completeLoading(inboundId, { expectedVersion: loadingVersion, remark: 'PDA确认装柜完成' }, tenantId)
if (!this.isEvidenceContextActive(contextId, inboundId)) return
this.showToast('已确认装柜完成', 'success')
} catch (error) {
if (!this.isEvidenceContextActive(contextId, inboundId)) return
this.remoteError = error.network ? '网络失败,装柜完成未提交' : (error.message || '装柜完成失败')
this.showToast('装柜完成失败', 'error')
}
finally {
if (this.isEvidenceContextActive(contextId, inboundId)) this.submitting = false
}
},
pickScannedBox(item, boxCode) {
const pickedCount = Number(this.partialQuantities[item.id] || 0) + 1
@@ -943,7 +1045,7 @@
this.submitting = true
try {
if (!this.remoteTaskId) throw new Error('尚未进入服务端拣货任务,请联网刷新后重试')
if (this.pickingVersion === null) throw new Error('缺少拣货版本号 pickingVersion,无法提交')
if (this.pickingVersion === null) throw new Error(VERSION_SYNC_MESSAGE)
const operationIdentity = stableTaskOperationIdentity(this.draftIdentity())
const submitId = this.submitId || stableOperationId('submit', operationIdentity, { itemStates: this.itemStates, partialQuantities: this.partialQuantities, incompleteReason })
this.submitId = submitId
@@ -958,7 +1060,8 @@
if (incompleteReason) return { type: 'SKIP', itemId, reason: `${incompleteReason}(提交时确认未拣)`, version: Number(item.version || 0) }
return null
}).filter(Boolean)
await submitContainerPickingBatch({ taskId: this.remoteTaskId, loadingPlanId: this.currentBox.loadingPlanId, containerBookingId: this.currentBox.containerBookingId, version: Number(this.pickingVersion) }, actions, { warehouseId, operationId: submitId, inventorySnapshotToken: inventorySnapshot?.snapshotToken })
const submittedTask = await submitContainerPickingBatch({ taskId: this.remoteTaskId, loadingPlanId: this.currentBox.loadingPlanId, containerBookingId: this.currentBox.containerBookingId, version: Number(this.pickingVersion) }, actions, { warehouseId, operationId: submitId, inventorySnapshotToken: inventorySnapshot?.snapshotToken })
if (submittedTask) this.remotePick = normalizePickingTask({ ...this.remotePick, ...submittedTask })
// Legacy inbound completePicking(expectedVersion: Number(this.pickingVersion)) remains adapter-only; cabinet tasks use item actions.
} catch (error) {
const versionConflict = error.code === 'A0409' || error.statusCode === 409
@@ -979,6 +1082,7 @@
item.finalPartialQuantity = this.partialQuantities[item.id] || 0
})
this.completedBoxes = { ...this.completedBoxes, [this.currentBoxIndex]: true }
this.loadLoadingEvidence()
this.saveCurrentBoxDraft()
if (remoteRequired) clearPickingDraft(this.draftIdentity())
this.submitId = ''