This commit is contained in:
2026-08-17 19:52:01 +08:00
parent 37d399752d
commit b2a62c4b70
5 changed files with 121 additions and 11 deletions
+86 -5
View File
@@ -150,6 +150,9 @@
<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>
@@ -353,7 +356,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, normalizePickingTask, submitContainerPickingBatch, uploadEvidenceFile, upsertEvidence, voidEvidence } from '@/api/pda-loading.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 { createEvidenceUploadQueue } from '@/utils/evidence-upload-queue.js'
import { isOnline } from '@/utils/network.js'
import { stableOperationId } from '@/utils/idempotency.js'
@@ -528,7 +531,7 @@
this.remoteTaskId = options.taskId ? String(options.taskId) : null
}
},
onShow() {
async onShow() {
if (!requireSession()) return
this.startHardwareScanner()
if (this.cameraPicking) {
@@ -540,7 +543,8 @@
this.debugEvidence('preview.resume', { evidenceIdentity: this.evidenceIdentity })
return
}
this.syncRemoteTask()
await this.refreshAvailableBoxes()
await this.syncRemoteTask()
},
onHide() {
if (this.evidencePreviewOpening) {
@@ -558,6 +562,67 @@
return false
},
methods: {
isSelectableTask(task) {
return Boolean(task?.recordStatus === 'ACTIVE'
&& !task?.mergedIntoContainerBookingId
&& task?.taskId
&& task?.loadingPlanId
&& task?.containerBookingId
&& task?.currentContainerNo
&& task?.warehouseId
&& task?.version !== null
&& task?.version !== undefined
&& ['UNASSIGNED', 'IN_PROGRESS', 'AWAITING_REVIEW'].includes(task?.workflowStatus))
},
boxFromTask(task, existing = null) {
return {
boxNo: String(task.currentContainerNo || existing?.boxNo || task.cabinetNo || '同步中'),
cabinetNo: task.cabinetNo ? String(task.cabinetNo) : (existing?.cabinetNo || null),
currentContainerNo: task.currentContainerNo ? String(task.currentContainerNo) : (existing?.currentContainerNo || null),
orderNo: String(task.displayNo || task.orderNo || existing?.orderNo || ''),
taskId: String(task.taskId),
loadingPlanId: task.loadingPlanId || existing?.loadingPlanId || null,
containerBookingId: task.containerBookingId || existing?.containerBookingId || null,
inboundId: existing?.inboundId || task.inboundId || null,
inboundVersion: existing?.inboundVersion ?? task.inboundVersion ?? null,
warehouseId: task.warehouseId || existing?.warehouseId || null,
tenantId: existing?.tenantId || task.tenantId || null,
items: existing?.items || []
}
},
remapBoxState(previousBoxes, nextBoxes, state) {
const byTaskId = new Map(previousBoxes.map((box, index) => [String(box.taskId || ''), state[index]]))
return Object.fromEntries(nextBoxes.map((box, index) => [index, byTaskId.get(String(box.taskId || ''))]).filter(([, value]) => value !== undefined))
},
async refreshAvailableBoxes() {
if (!session.token) return
try {
const result = await listLoadingOrders({ page: 1, size: 50 })
const records = (result.records || []).filter((task) => this.isSelectableTask(task))
const previousBoxes = [...this.boxes]
const selectedTaskId = String(this.remoteTaskId || this.currentBox.taskId || '')
const existingByTaskId = new Map(previousBoxes.map((box) => [String(box.taskId || ''), box]))
const recordByTaskId = new Map(records.map((task) => [String(task.taskId), task]))
const orderedTaskIds = previousBoxes.map((box) => String(box.taskId || '')).filter((taskId) => recordByTaskId.has(taskId))
records.forEach((task) => { const taskId = String(task.taskId); if (!orderedTaskIds.includes(taskId)) orderedTaskIds.push(taskId) })
if (selectedTaskId && !orderedTaskIds.includes(selectedTaskId) && existingByTaskId.has(selectedTaskId)) orderedTaskIds.unshift(selectedTaskId)
const nextBoxes = orderedTaskIds.map((taskId) => {
const existing = existingByTaskId.get(taskId) || null
const task = recordByTaskId.get(taskId)
return task ? this.boxFromTask(task, existing) : existing
}).filter(Boolean)
if (!nextBoxes.length) return
this.boxDrafts = this.remapBoxState(previousBoxes, nextBoxes, this.boxDrafts)
this.completedBoxes = this.remapBoxState(previousBoxes, nextBoxes, this.completedBoxes)
this.boxes = nextBoxes
const selectedIndex = this.boxes.findIndex((box) => String(box.taskId || '') === selectedTaskId)
this.currentBoxIndex = selectedIndex >= 0 ? selectedIndex : 0
this.debugSubmit('tasks.available', { selectedTaskId, taskIds: this.boxes.map((box) => box.taskId), count: this.boxes.length })
} catch (error) {
this.debugSubmit('tasks.available.error', { message: error.message || '', code: error.code || '', requestIdFromServer: error.requestId || '', traceId: error.traceId || '' })
this.showToast(error.network ? '任务列表同步失败,请检查网络后重试' : '任务列表同步失败,当前任务仍可继续', 'warning')
}
},
selectStage(stage) {
if (stage === 'evidence' && !this.loadingStageReady) {
this.showToast('请先完成新增待拣明细', 'warning')
@@ -953,6 +1018,7 @@
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
},
@@ -962,7 +1028,10 @@
if (item.status === 'PENDING') return '本地待上传'
if (item.status === 'UPLOADING') return `上传中 ${item.progress || 0}%`
if (item.status === 'FAILED') return item.error || '上传失败'
if (item.status === 'SUCCEEDED') return this.validEvidenceEntries(type).length ? '已上传并保存' : (item.attempts > 1 ? '重试成功' : '已上传')
if (item.status === 'SUCCEEDED') {
if (type === 'SEAL' && this.hasUnboundUploadedEvidence(type)) return '照片已上传,请输入封签号并保存'
return this.validEvidenceEntries(type).length ? '已上传并保存' : (item.attempts > 1 ? '重试成功' : '已上传')
}
if (item.status === 'REPLACED') return '已替换'
return ''
},
@@ -1203,6 +1272,11 @@
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)
},
async flushPendingEvidence() {
@@ -1210,7 +1284,8 @@
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 })))
for (const item of pending) {
if (!this.loadingStageReady) return
if (item.fileId) await this.saveEvidence(item.type, null, item.path)
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)
}
},
@@ -1228,10 +1303,16 @@
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.requireEvidenceStage()) return
if (type === 'SEAL' && !String(this.sealNo || '').trim()) { this.showToast('请输入封签号后点击保存封签', 'warning'); return }
if (!this.evidenceIdentity?.inboundId) { this.showToast('状态尚未同步,请刷新重试', 'warning'); return }
if (!isRemoteReady(this.evidenceIdentity.inboundId)) { this.showToast('当前会话未连接服务端,证据保留本地', 'warning'); return }
const contextId = this.evidenceContextId