fix(pda): bind loading evidence to cabinet tasks
This commit is contained in:
+74
-124
@@ -139,9 +139,6 @@
|
||||
<text v-if="evidenceQueueEntry(slot.type)" class="evidence-upload-state" :class="evidenceQueueEntry(slot.type).status.toLowerCase()">
|
||||
{{ evidenceUploadState(slot.type) }}
|
||||
</text>
|
||||
<text v-else-if="hasUnboundUploadedEvidence(slot.type)" class="evidence-upload-state pending">
|
||||
{{ slot.type === 'SEAL' ? '照片已上传,请输入封签号并保存' : '照片已上传,等待保存' }}
|
||||
</text>
|
||||
<text v-else-if="hasPendingLocalEvidence(slot.type)" class="evidence-upload-state pending">本地暂存,待自动上传</text>
|
||||
<button v-if="evidenceQueueEntry(slot.type)?.status === 'FAILED'" class="evidence-retry-btn" @tap="retryEvidence(slot.type)">重试上传</button>
|
||||
</view>
|
||||
@@ -151,8 +148,8 @@
|
||||
<button class="evidence-btn seal-btn" :disabled="sealSaving || !sealReadyToSave" @tap="saveSealNo">{{ sealSaving ? '保存中…' : '保存封签' }}</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 || !loadingStageReady || pickingVersion === null || loadingVersion === null || !evidenceComplete" @tap="completeLoadingTask">确认装柜完成</button>
|
||||
<view v-if="loadingVersion === null" class="remote-error-row">
|
||||
<button class="loading-complete-btn" :disabled="submitting || !loadingStageReady || pickingVersion === null || !evidenceComplete" @tap="completeLoadingTask">确认装柜完成</button>
|
||||
<view v-if="pickingVersion === null" class="remote-error-row">
|
||||
<text class="version-hint" role="status">状态尚未同步,请刷新重试</text>
|
||||
<button class="evidence-retry-btn" :disabled="remoteLoading" @tap="refreshTask">刷新任务</button>
|
||||
</view>
|
||||
@@ -341,7 +338,7 @@
|
||||
import { navigateBackOr } from '@/utils/navigation.js'
|
||||
import { startScanBroadcast } from '@/utils/scan-broadcast.js'
|
||||
import { playErrorSound, playSuccessSound } from '@/utils/audio-feedback.js'
|
||||
import { claimContainerPickingTask, completeContainerPickingTask, completePicking, downloadEvidencePreview, EVIDENCE_SLOTS, getContainerPickingTask, getEvidence, getEvidencePreviewUrl, getPickingInventorySnapshot, getPickingTask, isRemoteReady, listLoadingOrders, normalizePickingTask, submitContainerPickingBatch, uploadEvidenceFile, upsertEvidence, voidEvidence } from '@/api/pda-loading.js'
|
||||
import { claimContainerPickingTask, completeContainerPickingTask, downloadEvidencePreview, EVIDENCE_SLOTS, getContainerPickingTask, getEvidence, getEvidencePreviewUrl, getPickingInventorySnapshot, isRemoteReady, listLoadingOrders, normalizePickingTask, submitContainerPickingBatch, uploadEvidenceFile, voidEvidence } from '@/api/pda-loading.js'
|
||||
import { createEvidenceUploadQueue } from '@/utils/evidence-upload-queue.js'
|
||||
import { isOnline } from '@/utils/network.js'
|
||||
import { stableOperationId } from '@/utils/idempotency.js'
|
||||
@@ -388,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, evidencePreviewOpening: false, evidencePreviewReturning: false, sealNo: '', sealSaving: false, loadingVersion: null, pickingVersion: null, remotePick: null,
|
||||
evidenceFiles: {}, evidenceDrafts: {}, evidencePreviewCache: {}, evidenceQueueItems: [], evidenceQueue: null, evidenceContextId: 0, evidenceIdentity: null, cameraPicking: false, evidencePreviewOpening: false, evidencePreviewReturning: false, sealNo: '', sealSaving: false, pickingVersion: null, remotePick: null,
|
||||
remoteTaskId: null, assignmentVersion: null, submitId: '',
|
||||
taskRequestId: 0,
|
||||
remoteError: '', remoteLoading: false, submitting: false,
|
||||
@@ -433,16 +430,16 @@
|
||||
},
|
||||
pickingOperationReady() {
|
||||
return this.remotePick?.workflowStatus === 'IN_PROGRESS'
|
||||
&& Boolean(this.remoteTaskId && this.currentBox.inboundId && this.currentBox.loadingPlanId && this.currentBox.containerBookingId)
|
||||
&& Boolean(this.remoteTaskId && this.currentBox.loadingPlanId && this.currentBox.containerBookingId)
|
||||
&& this.pickingVersion !== null
|
||||
},
|
||||
loadingStageReady() {
|
||||
return this.pickingCompleted
|
||||
&& this.evidenceIdentity
|
||||
&& this.evidenceIdentity.inboundId
|
||||
&& this.loadingVersion !== null
|
||||
&& this.loadingVersion !== undefined
|
||||
&& Number.isFinite(Number(this.loadingVersion))
|
||||
&& this.evidenceIdentity.taskId
|
||||
&& this.evidenceIdentity.warehouseId
|
||||
&& this.pickingVersion !== null
|
||||
&& Number.isFinite(Number(this.pickingVersion))
|
||||
},
|
||||
partialTargetItem() {
|
||||
return this.currentBox.items.find((item) => item.id === this.partialTargetId) || null
|
||||
@@ -497,7 +494,7 @@
|
||||
: !this.completedBoxes[entry.index])
|
||||
},
|
||||
remoteSyncState() {
|
||||
if (!isRemoteReady(this.currentBox.inboundId || this.currentBox.boxNo)) return { label: '未连接', tone: 'error' }
|
||||
if (!isRemoteReady(this.remoteTaskId || this.currentBox.taskId)) return { label: '未连接', tone: 'error' }
|
||||
if (this.remoteLoading) return { label: '同步中', tone: 'loading' }
|
||||
if (this.remoteError) return { label: '同步失败', tone: 'error' }
|
||||
return { label: this.remotePick ? '已同步' : '待同步', tone: this.remotePick ? 'ready' : 'local' }
|
||||
@@ -506,7 +503,7 @@
|
||||
return this.evidenceSlots.every((slot) => this.validEvidenceEntries(slot.type).length > 0) && Boolean(String(this.sealNo || '').trim())
|
||||
},
|
||||
sealReadyToSave() {
|
||||
return Boolean(String(this.sealNo || '').trim()) && this.hasUnboundUploadedEvidence('SEAL')
|
||||
return Boolean(String(this.sealNo || '').trim()) && this.hasPendingLocalEvidence('SEAL')
|
||||
}
|
||||
},
|
||||
created() {
|
||||
@@ -636,10 +633,9 @@
|
||||
console.info('[PDA] picking.submit', {
|
||||
event,
|
||||
taskId: this.remoteTaskId,
|
||||
inboundId: this.currentBox.inboundId || this.evidenceIdentity?.inboundId || null,
|
||||
loadingPlanId: this.currentBox.loadingPlanId,
|
||||
containerBookingId: this.currentBox.containerBookingId,
|
||||
warehouseId: this.currentBox.warehouseId || session.currentUser?.warehouseId || session.currentUser?.warehouse,
|
||||
warehouseId: this.currentBox.warehouseId || null,
|
||||
pickingVersion: this.pickingVersion,
|
||||
pending: this.summary.pending,
|
||||
...payload
|
||||
@@ -665,7 +661,7 @@
|
||||
if (String(this.remoteTaskId || '') === String(requestedTaskId || '') && !this.remoteError) this.remoteError = message
|
||||
},
|
||||
async refreshInventorySnapshot() {
|
||||
const warehouseId = this.currentBox.warehouseId || session.currentUser?.warehouseId || session.currentUser?.warehouse
|
||||
const warehouseId = this.currentBox.warehouseId
|
||||
const snapshot = await getPickingInventorySnapshot(this.remoteTaskId, warehouseId)
|
||||
if (!snapshot?.snapshotToken) throw new Error('库存快照未同步,请刷新任务后重试')
|
||||
if (this.pickingVersion !== null && String(snapshot.version) !== String(this.pickingVersion)) {
|
||||
@@ -676,15 +672,11 @@
|
||||
return snapshot
|
||||
},
|
||||
captureEvidenceIdentity() {
|
||||
return {
|
||||
taskId: this.remoteTaskId || this.currentBox.taskId || null,
|
||||
inboundId: this.currentBox.inboundId || null,
|
||||
loadingPlanId: this.currentBox.loadingPlanId || null,
|
||||
containerBookingId: this.currentBox.containerBookingId || null
|
||||
}
|
||||
return { taskId: this.remoteTaskId || this.currentBox.taskId || null, warehouseId: this.currentBox.warehouseId || null }
|
||||
},
|
||||
isSameEvidenceIdentity(left, right) {
|
||||
return ['taskId', 'inboundId', 'loadingPlanId', 'containerBookingId'].every((key) => String(left?.[key] ?? '') === String(right?.[key] ?? ''))
|
||||
return String(left?.taskId ?? '') === String(right?.taskId ?? '')
|
||||
&& String(left?.warehouseId ?? '') === String(right?.warehouseId ?? '')
|
||||
},
|
||||
isMergedContainerRecord(task) {
|
||||
return task?.recordStatus === 'MERGED' || Boolean(task?.mergedIntoContainerBookingId)
|
||||
@@ -699,12 +691,7 @@
|
||||
warehouseId: previousIdentity?.warehouseId ?? this.currentBox.warehouseId
|
||||
}
|
||||
clearPickingDraft(staleIdentity)
|
||||
const evidenceKey = this.evidenceDraftKey({
|
||||
taskId: staleIdentity.taskId,
|
||||
inboundId: previousIdentity?.inboundId ?? task?.inboundId,
|
||||
loadingPlanId: staleIdentity.loadingPlanId,
|
||||
containerBookingId: staleIdentity.containerBookingId
|
||||
})
|
||||
const evidenceKey = this.evidenceDraftKey({ taskId: staleIdentity.taskId, warehouseId: staleIdentity.warehouseId })
|
||||
this.evidenceDrafts = Object.fromEntries(Object.entries(this.evidenceDrafts).filter(([key]) => key !== evidenceKey))
|
||||
this.evidencePreviewCache = Object.fromEntries(Object.entries(this.evidencePreviewCache).filter(([key]) => !key.startsWith(`${evidenceKey}|`)))
|
||||
this.taskRequestId += 1
|
||||
@@ -714,7 +701,6 @@
|
||||
this.remoteTaskId = null
|
||||
this.remotePick = null
|
||||
this.pickingVersion = null
|
||||
this.loadingVersion = null
|
||||
this.assignmentVersion = null
|
||||
this.itemStates = {}
|
||||
this.partialQuantities = {}
|
||||
@@ -737,11 +723,11 @@
|
||||
&& this.isSameEvidenceIdentity(this.evidenceIdentity, identity)
|
||||
},
|
||||
evidenceDraftKey(identity) {
|
||||
return ['taskId', 'inboundId', 'loadingPlanId', 'containerBookingId'].map((key) => String(identity?.[key] ?? '')).join('|')
|
||||
return `${String(identity?.taskId ?? '')}|${String(identity?.warehouseId ?? '')}`
|
||||
},
|
||||
rememberEvidenceDraft(identity = this.evidenceIdentity) {
|
||||
const key = this.evidenceDraftKey(identity)
|
||||
if (!key.replace(/\|/g, '')) return
|
||||
if (!identity?.taskId || !identity?.warehouseId) return
|
||||
const files = Object.fromEntries(Object.entries(this.evidenceFiles).map(([type, entries]) => [type, (Array.isArray(entries) ? entries : [entries]).filter((entry) => entry && !entry.id && entry.localPath).map((entry) => ({ ...entry }))]).filter(([, entries]) => entries.length))
|
||||
if (Object.keys(files).length || String(this.sealNo || '').trim()) this.evidenceDrafts = { ...this.evidenceDrafts, [key]: { files, sealNo: this.sealNo || '' } }
|
||||
},
|
||||
@@ -761,7 +747,7 @@
|
||||
this.evidenceDrafts = Object.keys(files).length || draft.sealNo ? { ...this.evidenceDrafts, [key]: { ...draft, files } } : Object.fromEntries(Object.entries(this.evidenceDrafts).filter(([draftKey]) => draftKey !== key))
|
||||
},
|
||||
requireEvidenceCapture() {
|
||||
if (!this.evidenceIdentity?.inboundId) {
|
||||
if (!this.evidenceIdentity?.taskId || !this.evidenceIdentity?.warehouseId) {
|
||||
this.showToast('任务状态尚未同步,请刷新重试', 'warning')
|
||||
return false
|
||||
}
|
||||
@@ -769,7 +755,7 @@
|
||||
},
|
||||
requireEvidenceMutation() {
|
||||
if (!this.requireEvidenceCapture()) return false
|
||||
if (!isRemoteReady(this.evidenceIdentity.inboundId)) {
|
||||
if (!isRemoteReady(this.evidenceIdentity.taskId)) {
|
||||
this.showToast('当前会话未连接服务端,证据保留本地', 'warning')
|
||||
return false
|
||||
}
|
||||
@@ -800,7 +786,7 @@
|
||||
this.sealNo = ''
|
||||
this.sealSaving = false
|
||||
this.evidenceQueue = createEvidenceUploadQueue({
|
||||
upload: (item, onProgress) => uploadEvidenceFile(item.inboundId, item.evidenceType, item.localPath, item.tenantId, onProgress),
|
||||
upload: (item, onProgress) => uploadEvidenceFile(item.taskId, item.warehouseId, item, item.localPath, onProgress),
|
||||
onChange: (items) => { if (this.evidenceContextId === contextId) this.evidenceQueueItems = items }
|
||||
})
|
||||
},
|
||||
@@ -814,7 +800,7 @@
|
||||
},
|
||||
draftIdentity() {
|
||||
const operatorId = session.currentUser?.id || session.currentUser?.userId || session.currentUser?.accountId
|
||||
return { operatorId, taskId: this.remoteTaskId || this.currentBox.taskId, inboundId: this.currentBox.inboundId || this.remotePick?.inboundId, loadingPlanId: this.currentBox.loadingPlanId || this.remotePick?.loadingPlanId, containerBookingId: this.currentBox.containerBookingId || this.remotePick?.containerBookingId, warehouseId: this.currentBox.warehouseId || session.currentUser?.warehouseId || session.currentUser?.warehouse }
|
||||
return { operatorId, taskId: this.remoteTaskId || this.currentBox.taskId, inboundId: this.currentBox.inboundId || this.remotePick?.inboundId, loadingPlanId: this.currentBox.loadingPlanId || this.remotePick?.loadingPlanId, containerBookingId: this.currentBox.containerBookingId || this.remotePick?.containerBookingId, warehouseId: this.currentBox.warehouseId || null }
|
||||
},
|
||||
restoreRemoteDraft() {
|
||||
const restored = restorePickingSession(this.draftIdentity())
|
||||
@@ -945,7 +931,6 @@
|
||||
this.remoteTaskId = this.currentBox.taskId || null
|
||||
this.remotePick = null
|
||||
this.pickingVersion = null
|
||||
this.loadingVersion = null
|
||||
this.assignmentVersion = null
|
||||
this.resetEvidenceState()
|
||||
this.boxDropdownOpen = false
|
||||
@@ -1024,7 +1009,11 @@
|
||||
evidenceItems(result) {
|
||||
const items = Array.isArray(result?.evidences) ? result.evidences : (Array.isArray(result?.evidences?.evidences) ? result.evidences.evidences : [])
|
||||
const seen = new Set()
|
||||
return items.filter((item) => {
|
||||
return items.map((item) => ({
|
||||
...item,
|
||||
id: String(item.evidenceId ?? item.id ?? ''),
|
||||
fileId: String(item.fileId ?? '')
|
||||
})).filter((item) => {
|
||||
const key = `${item.evidenceType || ''}:${item.fileId || `id-${item.id || ''}`}`
|
||||
if (seen.has(key)) return false
|
||||
seen.add(key)
|
||||
@@ -1034,7 +1023,6 @@
|
||||
validEvidenceEntries(type) { return this.evidenceEntries(type).filter((item) => item.id && item.fileId && item.uploadStatus !== 'PENDING') },
|
||||
evidenceEntry(type) { const list = this.evidenceEntries(type); return list.length ? list[list.length - 1] : null },
|
||||
hasPendingLocalEvidence(type) { return this.evidenceEntries(type).some((item) => item.localPath && !item.fileId) },
|
||||
hasUnboundUploadedEvidence(type) { return this.evidenceEntries(type).some((item) => item.fileId && !item.id) },
|
||||
evidenceQueueEntry(type) {
|
||||
return [...this.evidenceQueueItems].reverse().find((item) => item.evidenceType === type) || null
|
||||
},
|
||||
@@ -1045,7 +1033,6 @@
|
||||
if (item.status === 'UPLOADING') return `上传中 ${item.progress || 0}%`
|
||||
if (item.status === 'FAILED') return item.error || '上传失败'
|
||||
if (item.status === 'SUCCEEDED') {
|
||||
if (type === 'SEAL' && this.hasUnboundUploadedEvidence(type)) return '照片已上传,请输入封签号并保存'
|
||||
return this.validEvidenceEntries(type).length ? '已上传并保存' : (item.attempts > 1 ? '重试成功' : '已上传')
|
||||
}
|
||||
if (item.status === 'REPLACED') return '已替换'
|
||||
@@ -1069,13 +1056,12 @@
|
||||
this.currentBox.inboundId = null
|
||||
this.currentBox.loadingPlanId = null
|
||||
this.currentBox.containerBookingId = null
|
||||
this.loadingVersion = null
|
||||
this.resetEvidenceState()
|
||||
this.debugSubmit('refresh.start', { requestId, selector })
|
||||
this.remoteLoading = true
|
||||
this.remoteError = ''
|
||||
try {
|
||||
const warehouseId = this.currentBox.warehouseId || session.currentUser?.warehouseId || session.currentUser?.warehouse
|
||||
const warehouseId = this.currentBox.warehouseId
|
||||
const task = await getContainerPickingTask(selector, warehouseId)
|
||||
if (requestId !== this.taskRequestId) return
|
||||
if (!task) throw new Error('服务端未返回当前拣货任务')
|
||||
@@ -1087,7 +1073,7 @@
|
||||
if (task.recordStatus !== 'ACTIVE') 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.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
|
||||
@@ -1112,12 +1098,8 @@
|
||||
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 || task.orderNo || this.currentBox.orderNo
|
||||
this.currentBox.inboundCode = firstItem.inboundCode || task.inboundCode || this.currentBox.inboundCode
|
||||
this.currentBox.inboundId = task.inboundId
|
||||
this.currentBox.tenantId = evidenceItem.tenantId || null
|
||||
const serverStates = createItemStates(this.currentBox.items)
|
||||
const serverPartialQuantities = {}
|
||||
this.currentBox.items.forEach((item) => {
|
||||
@@ -1155,14 +1137,13 @@
|
||||
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 = this.remotePick.inboundVersion ?? this.remotePick.loadingVersion ?? null
|
||||
this.evidenceIdentity = this.captureEvidenceIdentity()
|
||||
this.restoreEvidenceDraft()
|
||||
if (this.isSameEvidenceIdentity(previousEvidenceState.identity, this.evidenceIdentity)) {
|
||||
this.evidenceFiles = { ...this.evidenceFiles, ...previousEvidenceState.files }
|
||||
if (previousEvidenceState.sealNo) this.sealNo = previousEvidenceState.sealNo
|
||||
}
|
||||
this.debugSubmit('refresh.ready', { requestId, workflowStatus: this.remotePick.workflowStatus, taskStatus: this.remotePick.taskStatus, assignmentVersion: this.assignmentVersion, loadingVersion: this.loadingVersion, evidenceIdentity: this.evidenceIdentity })
|
||||
this.debugSubmit('refresh.ready', { requestId, workflowStatus: this.remotePick.workflowStatus, taskStatus: this.remotePick.taskStatus, assignmentVersion: this.assignmentVersion, evidenceIdentity: this.evidenceIdentity })
|
||||
this.assignmentVersion = this.remotePick.assignment?.version ?? this.remotePick.assignmentVersion ?? this.assignmentVersion
|
||||
if (!this.pickingCompleted) this.restoreRemoteDraft()
|
||||
if (!this.pickingCompleted) {
|
||||
@@ -1186,15 +1167,15 @@
|
||||
} finally { if (requestId === this.taskRequestId) this.remoteLoading = false }
|
||||
},
|
||||
async loadLoadingEvidence() {
|
||||
if (!this.evidenceIdentity?.inboundId || !isRemoteReady(this.evidenceIdentity.inboundId)) return
|
||||
if (!this.evidenceIdentity?.taskId || !this.evidenceIdentity?.warehouseId || !isRemoteReady(this.evidenceIdentity.taskId)) return
|
||||
const contextId = this.evidenceContextId
|
||||
const identity = { ...this.evidenceIdentity }
|
||||
try {
|
||||
const evidence = await getEvidence(identity.inboundId, this.currentBox.tenantId)
|
||||
const evidence = await getEvidence(identity.taskId, identity.warehouseId)
|
||||
if (!this.isEvidenceContextActive(contextId, identity)) return
|
||||
const remoteItems = this.evidenceItems(evidence).map((item) => ({
|
||||
...item,
|
||||
previewUrl: item.previewUrl || getEvidencePreviewUrl(identity.inboundId, item.id, this.currentBox.tenantId)
|
||||
previewUrl: item.previewUrl || getEvidencePreviewUrl(identity.taskId, item.id, identity.warehouseId)
|
||||
}))
|
||||
this.debugEvidence('list.loaded', { evidenceIdentity: identity, count: remoteItems.length, previewCount: remoteItems.filter((item) => item.previewUrl).length })
|
||||
const localFiles = Object.fromEntries(Object.entries(this.evidenceFiles).map(([type, entries]) => [type, (Array.isArray(entries) ? entries : [entries]).filter((entry) => entry && !entry.id && entry.localPath)]))
|
||||
@@ -1261,6 +1242,10 @@
|
||||
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.rememberEvidenceDraft(identity)
|
||||
if (slot.type === 'SEAL') {
|
||||
this.showToast('带锁码照已拍摄,请输入封签号并保存', 'success')
|
||||
return
|
||||
}
|
||||
this.queueEvidenceUpload(slot.type, path)
|
||||
}, fail: (error) => {
|
||||
this.cameraPicking = false
|
||||
@@ -1269,36 +1254,41 @@
|
||||
this.showToast('未选择照片,可稍后重试', 'warning')
|
||||
} })
|
||||
},
|
||||
async queueEvidenceUpload(type, path) {
|
||||
if (!this.evidenceIdentity?.inboundId || !isRemoteReady(this.evidenceIdentity.inboundId)) { this.showToast('当前会话未连接服务端,证据保留本地', 'warning'); return }
|
||||
applyEvidenceResponse(result, type, localPath, identity) {
|
||||
const remoteItems = this.evidenceItems(result)
|
||||
const saved = [...remoteItems].reverse().find((item) => item.evidenceType === type && item.status !== 'VOID' && item.status !== 'VOIDED')
|
||||
if (saved?.fileId && localPath) this.cacheEvidencePreview(saved.fileId, localPath, identity)
|
||||
const localFiles = Object.fromEntries(Object.entries(this.evidenceFiles).map(([entryType, entries]) => [entryType, (Array.isArray(entries) ? entries : [entries]).filter((entry) => entry && !entry.id && entry.localPath && entry.localPath !== localPath)]).filter(([, entries]) => entries.length))
|
||||
this.evidenceFiles = remoteItems.map((item) => this.withCachedEvidencePreview(item, identity)).reduce((all, item) => { (all[item.evidenceType] ||= []).push(item); return all }, localFiles)
|
||||
const seal = this.evidenceEntry('SEAL')
|
||||
if (seal?.sealNo) this.sealNo = seal.sealNo
|
||||
this.removeDraftPhoto(type, localPath)
|
||||
},
|
||||
async queueEvidenceUpload(type, path, sealNo = null) {
|
||||
if (!this.evidenceIdentity?.taskId || !this.evidenceIdentity?.warehouseId || !isRemoteReady(this.evidenceIdentity.taskId)) { this.showToast('当前任务仓库尚未同步,证据保留本地', 'warning'); return }
|
||||
const contextId = this.evidenceContextId
|
||||
const identity = { ...this.evidenceIdentity }
|
||||
const queue = this.evidenceQueue
|
||||
const entry = queue.add({ ...identity, tenantId: this.currentBox.tenantId, evidenceType: type, localPath: path })
|
||||
const active = [...this.validEvidenceEntries(type)].reverse()[0]
|
||||
const expectedVersion = active?.version ?? 0
|
||||
const operationId = stableOperationId('evidence-upload', `${identity.taskId}|${identity.warehouseId}`, { evidenceType: type, expectedVersion, localPath: path, sealNo: sealNo || '' })
|
||||
const entry = queue.add({ taskId: identity.taskId, warehouseId: identity.warehouseId, evidenceType: type, expectedVersion, sealNo: sealNo || null, remark: null, operationId, localPath: path })
|
||||
this.debugSubmit('evidence.queue.start', { evidenceType: type, queueId: entry.id, evidenceIdentity: identity })
|
||||
await queue.flush()
|
||||
if (!this.isEvidenceContextActive(contextId, identity) || queue !== this.evidenceQueue) return
|
||||
const latest = queue.snapshot().find((item) => item.id === entry.id)
|
||||
this.debugSubmit('evidence.queue.result', { evidenceType: type, queueId: entry.id, status: latest?.status || 'missing', fileId: latest?.result?.fileId || null, error: latest?.error || '' })
|
||||
if (latest?.status !== 'SUCCEEDED') { this.remoteError = latest?.error || '文件上传失败'; return }
|
||||
const entries = this.evidenceFiles[type] || []
|
||||
const current = entries.find((item) => item.localPath === path) || {}
|
||||
this.evidenceFiles = { ...this.evidenceFiles, [type]: entries.map((item) => item.localPath === path ? { ...item, fileId: latest.result?.fileId, fileName: latest.result?.fileName || item.fileName, uploadStatus: 'SUCCEEDED' } : item) }
|
||||
if (type === 'SEAL') {
|
||||
this.rememberEvidenceDraft(identity)
|
||||
this.showToast('带锁码照已上传,请输入封签号并点击保存封签', 'success')
|
||||
return
|
||||
}
|
||||
await this.saveEvidence(type, null, path)
|
||||
this.applyEvidenceResponse(latest.result, type, path, identity)
|
||||
this.showToast(`${type === 'SEAL' ? '封签' : '照片'}已上传并保存`, 'success')
|
||||
},
|
||||
async flushPendingEvidence() {
|
||||
if (!this.evidenceIdentity?.inboundId) return
|
||||
const pending = this.evidenceSlots.flatMap((slot) => this.evidenceEntries(slot.type).filter((entry) => !entry.id && entry.localPath).map((entry) => ({ type: slot.type, path: entry.localPath, fileId: entry.fileId })))
|
||||
if (!this.evidenceIdentity?.taskId || !this.evidenceIdentity?.warehouseId) return
|
||||
const pending = this.evidenceSlots.flatMap((slot) => this.evidenceEntries(slot.type).filter((entry) => !entry.id && entry.localPath).map((entry) => ({ type: slot.type, path: entry.localPath })))
|
||||
for (const item of pending) {
|
||||
if (!this.evidenceIdentity?.inboundId) return
|
||||
if (item.fileId && item.type !== 'SEAL') await this.saveEvidence(item.type, null, item.path)
|
||||
else if (item.fileId) continue
|
||||
else await this.queueEvidenceUpload(item.type, item.path)
|
||||
if (!this.evidenceIdentity?.taskId || !this.evidenceIdentity?.warehouseId) return
|
||||
if (item.type === 'SEAL' && !String(this.sealNo || '').trim()) continue
|
||||
await this.queueEvidenceUpload(item.type, item.path, item.type === 'SEAL' ? this.sealNo : null)
|
||||
}
|
||||
},
|
||||
async retryEvidence(type) {
|
||||
@@ -1313,69 +1303,30 @@
|
||||
if (!this.isEvidenceContextActive(contextId, identity) || 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' }] }
|
||||
if (type === 'SEAL') {
|
||||
this.rememberEvidenceDraft(identity)
|
||||
this.showToast('带锁码照已上传,请输入封签号并点击保存封签', 'success')
|
||||
return
|
||||
}
|
||||
await this.saveEvidence(type)
|
||||
},
|
||||
async saveEvidence(type, operationId = null, localPath = null) {
|
||||
if (!this.requireEvidenceMutation()) return
|
||||
if (type === 'SEAL' && !String(this.sealNo || '').trim()) { this.showToast('请输入封签号后点击保存封签', 'warning'); return }
|
||||
const contextId = this.evidenceContextId
|
||||
const identity = { ...this.evidenceIdentity }
|
||||
const inboundId = identity.inboundId
|
||||
const tenantId = this.currentBox.tenantId
|
||||
const current = (localPath ? this.evidenceEntries(type).find((item) => item.localPath === localPath) : null) || 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-${identity.taskId || identity.inboundId}-${type}-${current.fileId || 'seal'}` }
|
||||
this.debugSubmit('evidence.bind.start', { evidenceType: type, fileId: current.fileId, expectedVersion: payload.expectedVersion, evidenceIdentity: identity })
|
||||
try {
|
||||
const result = await upsertEvidence(inboundId, payload, tenantId)
|
||||
if (!this.isEvidenceContextActive(contextId, identity)) return
|
||||
this.debugSubmit('evidence.bind.success', { evidenceType: type, fileId: current.fileId, evidenceCount: result?.evidences?.length || 0, evidenceIdentity: identity })
|
||||
this.cacheEvidencePreview(current.fileId, current.localPath, identity)
|
||||
this.evidenceFiles = this.evidenceItems(result).map((item) => this.withCachedEvidencePreview(item, identity)).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)
|
||||
if (current.localPath) this.evidenceFiles = { ...this.evidenceFiles, [type]: (this.evidenceFiles[type] || []).filter((entry) => entry.id || entry.localPath !== current.localPath) }
|
||||
this.removeDraftPhoto(type, current.localPath)
|
||||
this.showToast(`${type === 'SEAL' ? '封签' : '照片'}已保存`, 'success')
|
||||
} catch (error) {
|
||||
if (!this.isEvidenceContextActive(contextId, identity)) return
|
||||
if (this.isTaskConflict(error)) {
|
||||
await this.refreshAfterTaskConflict(sanitizeUserMessage(error.message, '任务内容已更新,请完成新增待拣项后重试'))
|
||||
return
|
||||
}
|
||||
this.remoteError = error.network ? '网络失败,证据未提交,请重试' : this.remoteFailureMessage(error, '证据保存失败')
|
||||
this.showToast('证据保存失败', 'error')
|
||||
}
|
||||
this.applyEvidenceResponse(latest.result, type, item.localPath, identity)
|
||||
this.showToast(`${type === 'SEAL' ? '封签' : '照片'}重试成功`, 'success')
|
||||
},
|
||||
async saveSealNo() {
|
||||
if (this.sealSaving) return
|
||||
if (!String(this.sealNo || '').trim()) { this.showToast('请输入封签号', 'warning'); return }
|
||||
this.rememberEvidenceDraft()
|
||||
const current = [...this.evidenceEntries('SEAL')].reverse().find((item) => item.fileId && !item.id)
|
||||
const current = [...this.evidenceEntries('SEAL')].reverse().find((item) => item.localPath && !item.id)
|
||||
if (!current) { this.showToast('当前封签已保存,无需重复提交', 'success'); return }
|
||||
const operationId = stableOperationId('seal', stableTaskOperationIdentity(this.draftIdentity()), { fileId: String(current.fileId), sealNo: String(this.sealNo).trim() })
|
||||
this.sealSaving = true
|
||||
try {
|
||||
await this.saveEvidence('SEAL', operationId, current.localPath || null)
|
||||
await this.queueEvidenceUpload('SEAL', current.localPath, String(this.sealNo).trim())
|
||||
} finally {
|
||||
this.sealSaving = false
|
||||
}
|
||||
},
|
||||
async voidEvidencePhoto(photo) {
|
||||
if (!this.requireEvidenceMutation()) return
|
||||
if (!photo?.id || !this.evidenceIdentity?.inboundId) return
|
||||
if (!photo?.id || !this.evidenceIdentity?.taskId || !this.evidenceIdentity?.warehouseId) return
|
||||
const contextId = this.evidenceContextId
|
||||
const identity = { ...this.evidenceIdentity }
|
||||
const inboundId = identity.inboundId
|
||||
const tenantId = this.currentBox.tenantId
|
||||
const reason = 'PDA现场标记错图'
|
||||
try {
|
||||
await voidEvidence(inboundId, photo.id, { expectedVersion: photo.version || 0, reason, operationId: `void-${inboundId}-${photo.id}-${photo.version || 0}` }, tenantId)
|
||||
await voidEvidence(identity.taskId, photo.id, identity.warehouseId, { expectedVersion: photo.version || 0, reason })
|
||||
if (!this.isEvidenceContextActive(contextId, identity)) 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')
|
||||
@@ -1391,8 +1342,8 @@
|
||||
},
|
||||
async completeLoadingTask() {
|
||||
if (!this.requireEvidenceStage()) return
|
||||
if (!this.evidenceIdentity?.inboundId || !isRemoteReady(this.evidenceIdentity.inboundId)) { this.showToast('装柜任务尚未同步,请刷新后重试', 'warning'); return }
|
||||
if (this.pickingVersion === null || this.loadingVersion === null) { this.showToast(VERSION_SYNC_MESSAGE, 'warning'); return }
|
||||
if (!this.evidenceIdentity?.taskId || !this.evidenceIdentity?.warehouseId || !isRemoteReady(this.evidenceIdentity.taskId)) { this.showToast('装柜任务或仓库尚未同步,请刷新后重试', 'warning'); return }
|
||||
if (this.pickingVersion === null) { this.showToast(VERSION_SYNC_MESSAGE, 'warning'); return }
|
||||
if (!this.evidenceComplete) { this.showToast('五类有效照片和封签号均为必填', 'warning'); return }
|
||||
const contextId = this.evidenceContextId
|
||||
const identity = { ...this.evidenceIdentity }
|
||||
@@ -1403,12 +1354,13 @@
|
||||
remark: serverAttempt.remark || 'PDA确认装柜完成'
|
||||
} : null)
|
||||
const expectedTaskVersion = savedAttempt?.expectedTaskVersion ?? Number(this.pickingVersion)
|
||||
const operationIdentity = stableTaskOperationIdentity(this.draftIdentity())
|
||||
const completionIdentity = this.draftIdentity()
|
||||
const operationIdentity = stableTaskOperationIdentity(completionIdentity)
|
||||
const operationId = savedAttempt?.operationId || stableOperationId('complete', operationIdentity, {
|
||||
taskId: this.remoteTaskId,
|
||||
expectedTaskVersion,
|
||||
loadingPlanId: identity.loadingPlanId,
|
||||
containerBookingId: identity.containerBookingId
|
||||
loadingPlanId: completionIdentity.loadingPlanId,
|
||||
containerBookingId: completionIdentity.containerBookingId
|
||||
})
|
||||
const completionAttempt = { operationId, expectedTaskVersion, remark: savedAttempt?.remark || 'PDA确认装柜完成' }
|
||||
saveLoadingCompletionAttempt(this.draftIdentity(), completionAttempt)
|
||||
@@ -1625,7 +1577,7 @@
|
||||
const operationIdentity = stableTaskOperationIdentity(this.draftIdentity())
|
||||
const submitId = this.submitId || stableOperationId('submit', operationIdentity, { itemStates: this.itemStates, partialQuantities: this.partialQuantities, incompleteReason })
|
||||
this.submitId = submitId
|
||||
const warehouseId = this.currentBox.warehouseId || session.currentUser?.warehouseId || session.currentUser?.warehouse
|
||||
const warehouseId = this.currentBox.warehouseId
|
||||
const inventorySnapshot = await this.refreshInventorySnapshot()
|
||||
this.debugSubmit('submit.inventory-snapshot', { snapshotTokenLength: inventorySnapshot?.snapshotToken ? String(inventorySnapshot.snapshotToken).length : 0, taskVersion: inventorySnapshot?.version ?? null })
|
||||
if (!inventorySnapshot?.snapshotToken) throw new Error('库存快照未同步,请刷新任务后重试')
|
||||
@@ -1635,7 +1587,6 @@
|
||||
if (submittedTask) {
|
||||
this.remotePick = normalizePickingTask({ ...this.remotePick, ...submittedTask })
|
||||
this.pickingVersion = this.remotePick.pickingVersion ?? this.remotePick.version ?? this.pickingVersion
|
||||
this.loadingVersion = submittedTask.inboundVersion ?? submittedTask.loadingVersion ?? this.remotePick.inboundVersion ?? this.remotePick.loadingVersion ?? this.loadingVersion
|
||||
this.evidenceIdentity = this.captureEvidenceIdentity()
|
||||
}
|
||||
this.showToast('拣货已提交,正在刷新最新状态', 'success')
|
||||
@@ -1645,7 +1596,6 @@
|
||||
this.showToast('拣货已提交,状态同步未完成,请刷新任务', 'warning')
|
||||
return
|
||||
}
|
||||
// Legacy inbound completePicking(expectedVersion: Number(this.pickingVersion)) remains adapter-only; cabinet tasks use item actions.
|
||||
} catch (error) {
|
||||
this.debugSubmit('submit.error', { message: error.message || '', code: error.code || '', statusCode: error.statusCode || '', requestIdFromServer: error.requestId || '', traceId: error.traceId || '', network: Boolean(error.network) })
|
||||
const versionConflict = this.isTaskConflict(error)
|
||||
|
||||
Reference in New Issue
Block a user