diff --git a/pages/picking/picking.vue b/pages/picking/picking.vue index 9a35d85..92d94fe 100644 --- a/pages/picking/picking.vue +++ b/pages/picking/picking.vue @@ -347,7 +347,7 @@ import { ITEM_STATUS, classifyScan, createItemStates, getPickSummary, submitStates } from '@/utils/picking-state.js' import { SKIP_REASONS, INCOMPLETE_REASONS } from '@/data/demo-data.js' import { session, requireSession } from '@/stores/session.js' - import { clearPickingDraft, getPickingSession, restorePickingSession, savePickingSession } from '@/stores/picking.js' + import { clearLoadingCompletionAttempt, clearPickingDraft, getPickingSession, restoreLoadingCompletionAttempt, restorePickingSession, saveLoadingCompletionAttempt, savePickingSession } from '@/stores/picking.js' import { navigateBackOr } from '@/utils/navigation.js' import { startScanBroadcast } from '@/utils/scan-broadcast.js' import { playErrorSound, playSuccessSound } from '@/utils/audio-feedback.js' @@ -356,6 +356,7 @@ import { isOnline } from '@/utils/network.js' import { stableOperationId } from '@/utils/idempotency.js' import { stableTaskOperationIdentity } from '@/utils/task-identity.js' + import { COMPLETION_PHASE, resolveCompletionPhase, shouldReconcileCompletion } from '@/utils/loading-completion-state.js' import ScanTargetInput from '@/components/ScanTargetInput.vue' import PopupHeader from '@/components/PopupHeader.vue' @@ -365,7 +366,11 @@ const workflowStatus = task?.workflowStatus const taskStatus = task?.taskStatus ?? task?.status return workflowStatus === 'AWAITING_REVIEW' + || workflowStatus === 'PROCESSING' + || workflowStatus === 'COMPLETION_FAILED' || workflowStatus === 'COMPLETED' + || taskStatus === 'COMPLETING' + || taskStatus === 'COMPLETION_FAILED' || taskStatus === 'COMPLETED' } @@ -1390,29 +1395,46 @@ if (!this.evidenceComplete) { this.showToast('五类有效照片和封签号均为必填', 'warning'); return } const contextId = this.evidenceContextId const identity = { ...this.evidenceIdentity } - const expectedTaskVersion = Number(this.pickingVersion) + const savedAttempt = restoreLoadingCompletionAttempt(this.draftIdentity()) + const expectedTaskVersion = savedAttempt?.expectedTaskVersion ?? Number(this.pickingVersion) const operationIdentity = stableTaskOperationIdentity(this.draftIdentity()) - const operationId = stableOperationId('complete', operationIdentity, { + const operationId = savedAttempt?.operationId || stableOperationId('complete', operationIdentity, { taskId: this.remoteTaskId, expectedTaskVersion, loadingPlanId: identity.loadingPlanId, containerBookingId: identity.containerBookingId }) + const completionAttempt = { operationId, expectedTaskVersion, remark: savedAttempt?.remark || 'PDA确认装柜完成' } + saveLoadingCompletionAttempt(this.draftIdentity(), completionAttempt) this.submitting = true try { const completedTask = await completeContainerPickingTask(this.remoteTaskId, { expectedTaskVersion, operationId, - remark: 'PDA确认装柜完成' + remark: completionAttempt.remark }) if (!this.isEvidenceContextActive(contextId, identity)) return if (completedTask) { this.remotePick = normalizePickingTask({ ...this.remotePick, ...completedTask }) this.pickingVersion = this.remotePick.pickingVersion ?? this.remotePick.version ?? this.pickingVersion } - this.showToast('已确认装柜完成', 'success') + const phase = resolveCompletionPhase({ taskStatus: completedTask?.taskStatus, commandStatus: completedTask?.commandStatus }) + if (phase === COMPLETION_PHASE.PROCESSING) { + this.showToast('完成命令已受理,后台处理中', 'success') + this.persistSession() + navigateBackOr('/pages/tasks/tasks') + return + } + if (phase === COMPLETION_PHASE.COMPLETED) clearLoadingCompletionAttempt(this.draftIdentity()) + this.showToast('装柜已完成', 'success') } catch (error) { if (!this.isEvidenceContextActive(contextId, identity)) return + if (shouldReconcileCompletion(error)) { + this.remoteError = '提交结果待确认,请返回任务列表刷新' + this.showToast('提交结果待确认', 'warning') + navigateBackOr('/pages/tasks/tasks') + return + } if (this.isTaskConflict(error)) { await this.refreshAfterTaskConflict(error.message || '订舱、装柜关联或证据状态已变化,请刷新后重试') return diff --git a/pages/tasks/tasks.vue b/pages/tasks/tasks.vue index 2b41f9f..0f6a288 100644 --- a/pages/tasks/tasks.vue +++ b/pages/tasks/tasks.vue @@ -46,11 +46,14 @@ onUnload() { if (this._stopNetwork) this._stopNetwork(); clearTimeout(this._toastTimer) }, methods: { statusLabel(status) { + if (status === 'PROCESSING') return '完成处理中' + if (status === 'COMPLETION_FAILED') return '完成失败' if (status === 'AWAITING_REVIEW') return '待上传证据/确认装柜' return status === 'IN_PROGRESS' ? '拣货中' : '待领取' }, taskActionLabel(task) { if (!this.isTaskReady(task)) return '任务暂不可进入' + if (task.workflowStatus === 'COMPLETION_FAILED') return '重试完成' return task.workflowStatus === 'AWAITING_REVIEW' ? '继续装柜取证' : '进入并开始拣货' }, isMergedTask(task) { return task?.recordStatus === 'MERGED' || Boolean(task?.mergedIntoContainerBookingId) }, @@ -58,7 +61,8 @@ const operatorId = session.currentUser?.id || session.currentUser?.userId || session.currentUser?.accountId clearPickingDraft({ operatorId, taskId: task.taskId, loadingPlanId: task.loadingPlanId, containerBookingId: task.containerBookingId, warehouseId: task.warehouseId }) }, - isTaskReady(task) { return Boolean(task.recordStatus === 'ACTIVE' && !task.mergedIntoContainerBookingId && task.taskId && task.loadingPlanId && task.containerBookingId && task.cabinetNo && task.currentContainerNo && task.warehouseId && task.version !== null && task.version !== undefined && Number.isInteger(Number(task.version)) && (task.workflowStatus === 'AWAITING_REVIEW' || (task.assignmentVersion !== null && task.assignmentVersion !== undefined && Number.isInteger(Number(task.assignmentVersion)))) && ['UNASSIGNED', 'IN_PROGRESS', 'AWAITING_REVIEW'].includes(task.workflowStatus)) }, + isTaskReady(task) { return Boolean(task.recordStatus === 'ACTIVE' && !task.mergedIntoContainerBookingId && task.taskId && task.loadingPlanId && task.containerBookingId && task.cabinetNo && task.currentContainerNo && task.warehouseId && task.version !== null && task.version !== undefined && Number.isInteger(Number(task.version)) && (['AWAITING_REVIEW', 'COMPLETION_FAILED'].includes(task.workflowStatus) || (task.assignmentVersion !== null && task.assignmentVersion !== undefined && Number.isInteger(Number(task.assignmentVersion)))) && ['UNASSIGNED', 'IN_PROGRESS', 'AWAITING_REVIEW', 'COMPLETION_FAILED'].includes(task.workflowStatus)) }, + isTaskVisible(task) { return Boolean(task.recordStatus === 'ACTIVE' && !task.mergedIntoContainerBookingId && task.taskId && ['UNASSIGNED', 'IN_PROGRESS', 'AWAITING_REVIEW', 'PROCESSING', 'COMPLETION_FAILED'].includes(task.workflowStatus)) }, async loadTasks() { if (!isOnline()) { this.error = '当前网络不可用,请联网后刷新'; return } this.loading = true; this.error = ''; this.notice = '' @@ -66,7 +70,7 @@ const result = await listLoadingOrders({ page: 1, size: 50 }) const records = result.records || [] records.filter((task) => this.isMergedTask(task)).forEach((task) => this.clearMergedTaskDraft(task)) - this.tasks = records.filter((task) => this.isTaskReady(task)) + this.tasks = records.filter((task) => this.isTaskVisible(task)) if (records.length > this.tasks.length) this.notice = '部分任务暂不可作业,请稍后刷新' } catch (error) { this.error = error.network ? '当前网络不可用,请联网后刷新' : (error.message || '任务加载失败') diff --git a/stores/picking.js b/stores/picking.js index 2373c30..af2fb25 100644 --- a/stores/picking.js +++ b/stores/picking.js @@ -19,6 +19,7 @@ function createInitialState() { let state = createInitialState() const STORAGE_PREFIX = 'pda-picking-draft' +const COMPLETION_PREFIX = 'pda-loading-completion' function storageAvailable() { return typeof uni !== 'undefined' && typeof uni.getStorageSync === 'function' && typeof uni.setStorageSync === 'function' @@ -80,6 +81,34 @@ export function clearPickingDraft(identity = {}) { if (key && typeof uni !== 'undefined' && typeof uni.removeStorageSync === 'function') uni.removeStorageSync(key) } +function completionKey(identity = {}) { + const identityKey = stableTaskIdentityKey(identity) + return identityKey ? `${COMPLETION_PREFIX}:${identityKey}` : '' +} + +export function saveLoadingCompletionAttempt(identity = {}, attempt = {}) { + const key = completionKey(identity) + if (!key || !storageAvailable() || !attempt.operationId) return + uni.setStorageSync(key, { + operationId: String(attempt.operationId), + expectedTaskVersion: Number(attempt.expectedTaskVersion), + remark: attempt.remark || '', + updatedAt: new Date().toISOString() + }) +} + +export function restoreLoadingCompletionAttempt(identity = {}) { + const key = completionKey(identity) + if (!key || !storageAvailable()) return null + const saved = uni.getStorageSync(key) + return saved?.operationId && Number.isInteger(Number(saved.expectedTaskVersion)) ? clone(saved) : null +} + +export function clearLoadingCompletionAttempt(identity = {}) { + const key = completionKey(identity) + if (key && typeof uni !== 'undefined' && typeof uni.removeStorageSync === 'function') uni.removeStorageSync(key) +} + export function resetPickingSession() { state = createInitialState() }