diff --git a/api/pda-loading.js b/api/pda-loading.js index 7b43eed..7d7cfce 100644 --- a/api/pda-loading.js +++ b/api/pda-loading.js @@ -183,6 +183,9 @@ export function normalizePdaOrder(row = {}) { taskId: row.taskId ?? row.pdaTaskId ?? row.loadingTaskId ?? row.loading?.taskId ?? null, loadingPlanId: row.loadingPlanId ?? row.loading?.loadingPlanId ?? row.loadingPlan?.id ?? null, containerBookingId: row.containerBookingId ?? row.loading?.containerBookingId ?? row.container?.id ?? null, + sourceType: row.sourceType ?? row.container?.sourceType ?? null, + recordStatus: row.recordStatus ?? row.container?.recordStatus ?? null, + mergedIntoContainerBookingId: row.mergedIntoContainerBookingId ?? row.container?.mergedIntoContainerBookingId ?? null, cabinetNo: row.cabinetNo ?? row.loading?.cabinetNo ?? row.container?.cabinetNo ?? row.containerNo ?? '', currentContainerNo: row.currentContainerNo ?? row.containerNo ?? row.container?.containerNo ?? row.loadingPlan?.containerNo ?? row.loading?.containerNo ?? null, warehouseId: row.warehouseId ?? row.warehouse?.id ?? row.loadingPlan?.warehouseId ?? null, @@ -197,6 +200,9 @@ export function normalizePickingTask(task = {}) { taskId: task.taskId ?? task.id ?? null, loadingPlanId: task.loadingPlanId ?? task.loading?.loadingPlanId ?? null, containerBookingId: task.containerBookingId ?? task.loading?.containerBookingId ?? null, + sourceType: task.sourceType ?? task.container?.sourceType ?? null, + recordStatus: task.recordStatus ?? task.container?.recordStatus ?? null, + mergedIntoContainerBookingId: task.mergedIntoContainerBookingId ?? task.container?.mergedIntoContainerBookingId ?? null, cabinetNo: task.cabinetNo ?? task.loading?.cabinetNo ?? task.containerNo ?? '', currentContainerNo: task.currentContainerNo ?? task.containerNo ?? null, pickingVersion: task.pickingVersion ?? task.version ?? null, diff --git a/pages/picking/picking.vue b/pages/picking/picking.vue index 736f8ad..25e34d2 100644 --- a/pages/picking/picking.vue +++ b/pages/picking/picking.vue @@ -574,6 +574,52 @@ isSameEvidenceIdentity(left, right) { return ['taskId', 'inboundId', 'loadingPlanId', 'containerBookingId'].every((key) => String(left?.[key] ?? '') === String(right?.[key] ?? '')) }, + isMergedContainerRecord(task) { + return task?.recordStatus === 'MERGED' || Boolean(task?.mergedIntoContainerBookingId) + }, + clearMergedTask(task, previousIdentity) { + const operatorId = session.currentUser?.id || session.currentUser?.userId || session.currentUser?.accountId + const staleIdentity = { + operatorId, + taskId: previousIdentity?.taskId ?? task?.taskId ?? task?.id, + loadingPlanId: previousIdentity?.loadingPlanId ?? task?.loadingPlanId, + containerBookingId: previousIdentity?.containerBookingId ?? task?.containerBookingId, + 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 + }) + 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 + this.remoteLoading = false + this.submitting = false + this.remoteError = '当前柜记录已合并,请返回任务列表刷新' + this.remoteTaskId = null + this.remotePick = null + this.pickingVersion = null + this.loadingVersion = null + this.assignmentVersion = null + this.itemStates = {} + this.partialQuantities = {} + this.scannedBoxCodes = {} + this.boxDrafts = {} + this.completedBoxes = {} + this.scanHistory = [] + this.currentBox.taskId = null + this.currentBox.inboundId = null + this.currentBox.loadingPlanId = null + this.currentBox.containerBookingId = null + this.currentBox.cabinetNo = '' + this.currentBox.boxNo = '' + this.currentBox.currentContainerNo = null + this.currentBox.items = [] + this.resetEvidenceState() + }, isEvidenceContextActive(contextId, identity) { return contextId === this.evidenceContextId && this.isSameEvidenceIdentity(this.evidenceIdentity, identity) @@ -647,7 +693,7 @@ }, draftIdentity() { const operatorId = session.currentUser?.id || session.currentUser?.userId || session.currentUser?.accountId - return { operatorId, taskId: this.remoteTaskId || this.currentBox.taskId, 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 || session.currentUser?.warehouseId || session.currentUser?.warehouse } }, restoreRemoteDraft() { const restored = restorePickingSession(this.draftIdentity()) @@ -864,6 +910,7 @@ const requestedTaskId = this.remoteTaskId || this.currentBox.taskId if (!requestedTaskId) { this.remoteError = '当前没有可进入的拣货任务,请返回任务列表刷新'; return } this.rememberEvidenceDraft() + const previousIdentity = this.draftIdentity() const requestId = ++this.taskRequestId const selector = { taskId: requestedTaskId } this.currentBox.inboundId = null @@ -879,6 +926,12 @@ const task = await getContainerPickingTask(selector, warehouseId) if (requestId !== this.taskRequestId) return if (!task) throw new Error('服务端未返回当前拣货任务') + if (this.isMergedContainerRecord(task)) { + this.clearMergedTask(task, previousIdentity) + this.showToast('当前柜记录已合并,请返回任务列表刷新', 'warning') + return + } + 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('任务尚未同步完整作业关联,请返回任务列表刷新') diff --git a/pages/tasks/tasks.vue b/pages/tasks/tasks.vue index efb8a06..26a8477 100644 --- a/pages/tasks/tasks.vue +++ b/pages/tasks/tasks.vue @@ -26,6 +26,7 @@ import PortalHeader from '@/components/PortalHeader.vue' import { listLoadingOrders } from '@/api/pda-loading.js' import { session, requireSession } from '@/stores/session.js' + import { clearPickingDraft } from '@/stores/picking.js' import { isOnline, watchNetwork } from '@/utils/network.js' export default { @@ -36,13 +37,19 @@ onUnload() { if (this._stopNetwork) this._stopNetwork(); clearTimeout(this._toastTimer) }, methods: { statusLabel(status) { return status === 'IN_PROGRESS' ? '拣货中' : '待领取' }, - isTaskReady(task) { return Boolean(task.taskId && task.loadingPlanId && task.containerBookingId && task.cabinetNo && task.currentContainerNo && task.warehouseId && task.version !== null && task.version !== undefined && Number.isInteger(Number(task.version)) && task.assignmentVersion !== null && task.assignmentVersion !== undefined && Number.isInteger(Number(task.assignmentVersion)) && ['UNASSIGNED', 'IN_PROGRESS'].includes(task.workflowStatus)) }, + isMergedTask(task) { return task?.recordStatus === 'MERGED' || Boolean(task?.mergedIntoContainerBookingId) }, + clearMergedTaskDraft(task) { + 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.assignmentVersion !== null && task.assignmentVersion !== undefined && Number.isInteger(Number(task.assignmentVersion)) && ['UNASSIGNED', 'IN_PROGRESS'].includes(task.workflowStatus)) }, async loadTasks() { if (!isOnline()) { this.error = '当前网络不可用,请联网后刷新'; return } this.loading = true; this.error = ''; this.notice = '' try { 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)) if (records.length > this.tasks.length) this.notice = '部分任务尚未完成服务端就绪校验,本机已隐藏,请稍后刷新' } catch (error) { diff --git a/tests/page-contract.test.mjs b/tests/page-contract.test.mjs index b43072c..56839e6 100644 --- a/tests/page-contract.test.mjs +++ b/tests/page-contract.test.mjs @@ -47,6 +47,9 @@ test('picking task list uses remote loading orders and never falls back to demo assert.match(tasks, /集装箱号 \{\{ task\.currentContainerNo \}\}/) assert.match(tasks, /柜次号 \{\{ task\.cabinetNo \}\}/) assert.match(tasks, /records\.filter\(\(task\) => this\.isTaskReady\(task\)\)/) + assert.match(tasks, /isTaskReady\(task\)[\s\S]*?task\.recordStatus === 'ACTIVE'[\s\S]*?!task\.mergedIntoContainerBookingId/) + assert.match(tasks, /records\.filter\(\(task\) => this\.isMergedTask\(task\)\)\.forEach\(\(task\) => this\.clearMergedTaskDraft\(task\)\)/) + assert.match(tasks, /clearMergedTaskDraft\(task\)[\s\S]*?clearPickingDraft\(\{ operatorId, taskId: task\.taskId, loadingPlanId: task\.loadingPlanId, containerBookingId: task\.containerBookingId/) assert.match(tasks, /task\.taskId[\s\S]*?task\.loadingPlanId[\s\S]*?task\.containerBookingId[\s\S]*?task\.currentContainerNo[\s\S]*?task\.version[\s\S]*?task\.assignmentVersion/) assert.doesNotMatch(tasks, /task\.taskStatus === 'LOADING'/) assert.doesNotMatch(tasks, /demo-data|DEMO_/) diff --git a/tests/pda-loading-api.test.mjs b/tests/pda-loading-api.test.mjs index 9687c7b..237329e 100644 --- a/tests/pda-loading-api.test.mjs +++ b/tests/pda-loading-api.test.mjs @@ -48,6 +48,13 @@ test('C02 identifiers and separate picking/inbound versions are normalized expli assert.match(page, /状态尚未同步,请刷新重试/) }) +test('PDA normalizes the frozen active and merged container identity fields', () => { + for (const field of ['sourceType', 'recordStatus', 'mergedIntoContainerBookingId']) { + assert.match(source, new RegExp(`${field}: row\\.${field} \\?\\? row\\.container\\?\\.${field}`)) + assert.match(source, new RegExp(`${field}: task\\.${field} \\?\\? task\\.container\\?\\.${field}`)) + } +}) + test('picking page exposes five evidence slots, retry and remote completion states', () => { for (const text of ['PHOTO_STEP_1', 'PHOTO_STEP_4', 'SEAL', '网络失败', '确认装柜完成', 'loadingVersion']) { assert.match(`${source}\n${page}`, new RegExp(text)) diff --git a/tests/picking-evidence-stage.test.mjs b/tests/picking-evidence-stage.test.mjs index cf6e83f..bf45cca 100644 --- a/tests/picking-evidence-stage.test.mjs +++ b/tests/picking-evidence-stage.test.mjs @@ -59,6 +59,20 @@ test('evidence identity is bound to task, inbound, loading plan and container bo assert.match(page, /this\.evidenceIdentity = this\.captureEvidenceIdentity\(\)/) }) +test('merged or non-active cabinet details are rejected before claim and clear stale local state', () => { + const sync = page.match(/async syncRemoteTask\(\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || '' + assert.match(sync, /if \(this\.isMergedContainerRecord\(task\)\)[\s\S]*?this\.clearMergedTask\(task, previousIdentity\)[\s\S]*?return[\s\S]*?task\.recordStatus !== 'ACTIVE'[\s\S]*?claimContainerPickingTask/) + const clear = page.match(/clearMergedTask\(task, previousIdentity\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || '' + assert.match(clear, /taskId: previousIdentity\?\.taskId \?\? task\?\.taskId/) + assert.match(clear, /inboundId: previousIdentity\?\.inboundId \?\? task\?\.inboundId/) + assert.match(clear, /loadingPlanId: previousIdentity\?\.loadingPlanId \?\? task\?\.loadingPlanId/) + assert.match(clear, /containerBookingId: previousIdentity\?\.containerBookingId \?\? task\?\.containerBookingId/) + assert.match(clear, /clearPickingDraft\(staleIdentity\)/) + assert.match(clear, /this\.evidenceDrafts = Object\.fromEntries/) + assert.match(clear, /this\.evidencePreviewCache = Object\.fromEntries/) + for (const reset of ['remoteLoading = false', 'submitting = false', 'remoteTaskId = null', 'loadingVersion = null', 'scanHistory = []', 'currentBox.containerBookingId = null', 'currentBox.currentContainerNo = null', 'resetEvidenceState()']) assert.match(clear, new RegExp(reset.replace(/[.()[\]]/g, '\\$&'))) +}) + test('server evidence mutations remain closed before the loading stage is ready', () => { assert.match(page, /requireEvidenceStage\(\)[\s\S]*?if \(!this\.pickingCompleted\)[\s\S]*?请先提交拣货完成[\s\S]*?if \(!this\.loadingStageReady\)/) const queue = page.match(/async queueEvidenceUpload\([^)]*\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''