This commit is contained in:
2026-08-17 13:24:58 +08:00
parent c8bb1eecdf
commit 6bbbcb7750
6 changed files with 92 additions and 2 deletions
+6
View File
@@ -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,
+54 -1
View File
@@ -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('任务尚未同步完整作业关联,请返回任务列表刷新')
+8 -1
View File
@@ -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) {
+3
View File
@@ -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_/)
+7
View File
@@ -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))
+14
View File
@@ -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] || ''