From 3bb5da8ae79ff73fb1be692cdf0868f2db6558fe Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Sat, 15 Aug 2026 17:04:00 +0800 Subject: [PATCH] feat: gate PDA loading evidence by ready task --- api/pda-loading.js | 25 ++--- pages/picking/picking.vue | 136 +++++++++++++++++--------- pages/tasks/tasks.vue | 20 ++-- tests/page-contract.test.mjs | 8 +- tests/pda-loading-api.test.mjs | 10 +- tests/picking-evidence-stage.test.mjs | 27 ++++- 6 files changed, 147 insertions(+), 79 deletions(-) diff --git a/api/pda-loading.js b/api/pda-loading.js index 8b0a96f..264a626 100644 --- a/api/pda-loading.js +++ b/api/pda-loading.js @@ -87,23 +87,12 @@ export function isRemoteReady(inboundId) { export function getContainerPickingTask(identity, warehouseId) { const selector = typeof identity === 'object' && identity !== null ? identity : {} const taskId = selector.taskId ?? selector.id - if (taskId) { - if (!warehouseId) throw new Error('缺少作业仓库标识,无法请求现场任务') - return request({ - url: `/first-leg/pda-loading/tasks/${encodeURIComponent(taskId)}?warehouseId=${encodeURIComponent(warehouseId)}`, - method: 'GET' - }).then(normalizePickingTask) - } - const query = [] - if (selector.cabinetNo) query.push(`cabinetNo=${encodeURIComponent(selector.cabinetNo)}`) - else if (selector.loadingPlanId) query.push(`loadingPlanId=${encodeURIComponent(selector.loadingPlanId)}`) - else throw new Error('缺少稳定柜次标识,无法请求现场任务') - if (warehouseId) query.push(`warehouseId=${encodeURIComponent(warehouseId)}`) - if (session.tenantId) query.push(`tenantId=${encodeURIComponent(session.tenantId)}`) - return request({ url: `/first-leg/pda-loading/tasks?${query.join('&')}`, method: 'GET' }).then((data) => { - const task = Array.isArray(data) ? data[0] : (data?.records ? data.records[0] : data) - return task ? normalizePickingTask(task) : null - }) + if (!taskId) throw new Error('当前没有可进入的拣货任务,请返回任务列表刷新') + if (!warehouseId) throw new Error('缺少作业仓库标识,无法请求现场任务') + return request({ + url: `/first-leg/pda-loading/tasks/${encodeURIComponent(taskId)}?warehouseId=${encodeURIComponent(warehouseId)}`, + method: 'GET' + }).then(normalizePickingTask) } export function claimContainerPickingTask(taskId, expectedAssignmentVersion, operationId) { @@ -197,7 +186,7 @@ export function normalizePdaOrder(row = {}) { 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, - displayNo: row.orderNo ?? row.inboundCode ?? row.order?.orderNo ?? `任务 ${row.taskId ?? row.id}` + displayNo: row.orderNo ?? row.inboundCode ?? row.order?.orderNo ?? '业务单号待同步' } } diff --git a/pages/picking/picking.vue b/pages/picking/picking.vue index de25308..63c45c4 100644 --- a/pages/picking/picking.vue +++ b/pages/picking/picking.vue @@ -14,18 +14,18 @@ - {{ box.boxNo }}{{ box.orderNo }} + {{ box.currentContainerNo || '集装箱号同步中' }}柜次号 {{ box.cabinetNo || '同步中' }} · {{ box.orderNo }} 已完成 有草稿 @@ -70,7 +70,7 @@ / {{ summary.total }} 箱 - + @@ -81,7 +81,7 @@ □ - 该箱号暂无拣货任务 + 当前集装箱暂无拣货明细 ✓ - 箱号 {{ currentBox.boxNo }} · 订单 {{ currentBox.orderNo }} + 集装箱号 {{ currentBox.currentContainerNo }} · 柜次号 {{ currentBox.cabinetNo }} · 订单 {{ currentBox.orderNo }} {{ completionSummary.picked }} @@ -324,10 +324,10 @@ - - + + ✓ - 本班次演示任务 {{ boxes.length }} 箱已全部处理 + 本班次 {{ boxes.length }} 个柜次任务已全部处理 @@ -385,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, sealNo: '', loadingVersion: null, pickingVersion: null, remotePick: null, + evidenceFiles: {}, evidenceDrafts: {}, evidencePreviewCache: {}, evidenceQueueItems: [], evidenceQueue: null, evidenceContextId: 0, evidenceIdentity: null, cameraPicking: false, evidencePreviewOpening: false, evidencePreviewReturning: false, sealNo: '', loadingVersion: null, pickingVersion: null, remotePick: null, remoteTaskId: null, assignmentVersion: null, submitId: '', taskRequestId: 0, remoteError: '', remoteLoading: false, submitting: false, @@ -424,6 +424,11 @@ return Boolean(this.completedBoxes[this.currentBoxIndex]) || isPickingCompleteTask(this.remotePick) }, + pickingOperationReady() { + return this.remotePick?.workflowStatus === 'IN_PROGRESS' + && Boolean(this.remoteTaskId && this.currentBox.inboundId && this.currentBox.loadingPlanId && this.currentBox.containerBookingId) + && this.pickingVersion !== null + }, loadingStageReady() { return this.pickingCompleted && this.evidenceIdentity @@ -461,10 +466,10 @@ return '' }, submitButtonText() { - return this.completedBoxes[this.currentBoxIndex] || this.pickingCompleted ? '该箱已完成' : this.summary.buttonText + return this.completedBoxes[this.currentBoxIndex] || this.pickingCompleted ? '该柜次已完成' : this.summary.buttonText }, submitStatusHint() { - if (this.completedBoxes[this.currentBoxIndex] || this.pickingCompleted) return '本箱任务已完成' + if (this.completedBoxes[this.currentBoxIndex] || this.pickingCompleted) return '本柜次任务已完成' if (!this.summary.canSubmit) return '请先处理至少一件' if (this.summary.pending > 0) return '提交时需选择原因' return `已处理 ${this.summary.processed}/${this.summary.total}` @@ -494,10 +499,10 @@ }, onLoad(options = {}) { if (!requireSession()) return - if (options.taskId && (options.cabinetNo || options.containerNo)) { + if (options.taskId) { this.boxes = [{ - boxNo: String(options.cabinetNo || options.containerNo), cabinetNo: String(options.cabinetNo || options.containerNo), currentContainerNo: options.currentContainerNo ? String(options.currentContainerNo) : null, orderNo: String(options.orderNo || options.inboundCode || ''), - taskId: String(options.taskId), loadingPlanId: options.loadingPlanId ? String(options.loadingPlanId) : null, containerBookingId: options.containerBookingId ? String(options.containerBookingId) : null, + boxNo: String(options.currentContainerNo || options.cabinetNo || '同步中'), cabinetNo: options.cabinetNo ? String(options.cabinetNo) : null, currentContainerNo: options.currentContainerNo ? String(options.currentContainerNo) : null, orderNo: String(options.orderNo || options.inboundCode || ''), + taskId: String(options.taskId), loadingPlanId: null, containerBookingId: null, inboundId: options.inboundId ? String(options.inboundId) : null, inboundVersion: options.inboundVersion == null ? null : Number(options.inboundVersion), warehouseId: options.warehouseId ? String(options.warehouseId) : null, @@ -507,16 +512,27 @@ this.remoteTaskId = options.taskId ? String(options.taskId) : null } }, - onShow() { - if (!requireSession()) return - this.startHardwareScanner() - if (this.cameraPicking) { - this.debugEvidence('choose.resume', { evidenceIdentity: this.evidenceIdentity }) - return - } - this.syncRemoteTask() - }, - onHide() { this.stopHardwareScanner() }, + onShow() { + if (!requireSession()) return + this.startHardwareScanner() + if (this.cameraPicking) { + this.debugEvidence('choose.resume', { evidenceIdentity: this.evidenceIdentity }) + return + } + if (this.evidencePreviewReturning) { + this.evidencePreviewReturning = false + this.debugEvidence('preview.resume', { evidenceIdentity: this.evidenceIdentity }) + return + } + this.syncRemoteTask() + }, + onHide() { + if (this.evidencePreviewOpening) { + this.evidencePreviewOpening = false + this.evidencePreviewReturning = true + } + this.stopHardwareScanner() + }, onUnload() { this.stopHardwareScanner(); this.persistSession(); this.clearTimers() }, beforeUnmount() { this.stopHardwareScanner(); this.clearTimers() }, onBackPress() { @@ -593,6 +609,11 @@ } return true }, + requirePickingOperation() { + if (this.pickingOperationReady) return true + this.showToast('任务尚未领取完成,请稍候或刷新重试', 'warning') + return false + }, requireEvidenceStage() { if (!this.pickingCompleted) { this.showToast('请先提交拣货完成', 'warning') @@ -680,7 +701,7 @@ savePickingSession({ boxes: this.boxes, currentBoxIndex: this.currentBoxIndex, itemStates: this.itemStates, partialQuantities: this.partialQuantities, scannedBoxCodes: this.scannedBoxCodes, boxDrafts: this.boxDrafts, completedBoxes: this.completedBoxes, scanValue: this.scanValue, scanHistory: this.scanHistory }, this.draftIdentity()) }, leavePicking() { this.persistSession(); this.activeOverlay = ''; navigateBackOr('/pages/home/home') }, - clearTimers() { clearTimeout(this._toastTimer) }, + clearTimers() { clearTimeout(this._toastTimer); clearTimeout(this._evidencePreviewOpenTimer) }, scrollToTop() { this.$nextTick(() => { if (typeof uni !== 'undefined' && typeof uni.pageScrollTo === 'function') uni.pageScrollTo({ scrollTop: 0, duration: 0 }) }) }, saveCurrentBoxDraft() { if (!this.currentBox.boxNo) return @@ -739,7 +760,7 @@ this.activeStage = 'picking' this.currentBox.inboundId = null this.currentBox.inboundVersion = null - this.remoteTaskId = null + this.remoteTaskId = this.currentBox.taskId || null this.remotePick = null this.pickingVersion = null this.loadingVersion = null @@ -748,8 +769,8 @@ this.boxDropdownOpen = false this.loadBoxState(index) this.scrollToTop() - if (session.token && session.tenantId && this.currentBox.boxNo) this.syncRemoteTask() - this.showToast(`切换至 ${this.currentBox.boxNo}`, 'success') + if (session.token && this.currentBox.taskId) this.syncRemoteTask() + this.showToast(`切换至集装箱 ${this.currentBox.currentContainerNo || '同步中'}`, 'success') }, itemState(item) { return this.itemStates[item.id] || ITEM_STATUS.PENDING @@ -765,6 +786,7 @@ return `${item.desc}${reason}` }, processScannedValue(scanValue, source = 'broadcast') { + if (!this.requirePickingOperation()) return const value = scanValue.trim() if (!value) { const manual = source === 'manual' @@ -812,7 +834,10 @@ if (!current || !urls.length) { this.showToast('照片地址尚未同步,请刷新后重试', 'warning'); return } if (typeof uni.previewImage !== 'function') { this.showToast('当前环境不支持预览照片', 'warning'); return } this.debugEvidence('preview.open', { evidenceType: type, evidenceIdentity: this.evidenceIdentity, count: urls.length }) - uni.previewImage({ current, urls }) + this.evidencePreviewOpening = true + clearTimeout(this._evidencePreviewOpenTimer) + this._evidencePreviewOpenTimer = setTimeout(() => { this.evidencePreviewOpening = false }, 1500) + uni.previewImage({ current, urls, fail: () => { this.evidencePreviewOpening = false } }) }, evidenceItems(result) { if (Array.isArray(result?.evidences)) return result.evidences @@ -835,25 +860,33 @@ return '' }, async syncRemoteTask() { - if (!isRemoteReady(this.currentBox.inboundId || this.currentBox.boxNo)) return + if (!session.token) return + const requestedTaskId = this.remoteTaskId || this.currentBox.taskId + if (!requestedTaskId) { this.remoteError = '当前没有可进入的拣货任务,请返回任务列表刷新'; return } this.rememberEvidenceDraft() const requestId = ++this.taskRequestId - const selector = { taskId: this.remoteTaskId || this.currentBox.taskId, cabinetNo: this.currentBox.cabinetNo, loadingPlanId: this.currentBox.loadingPlanId } + const selector = { taskId: requestedTaskId } this.currentBox.inboundId = null this.currentBox.loadingPlanId = null this.currentBox.containerBookingId = null this.loadingVersion = null this.resetEvidenceState() - this.debugSubmit('refresh.start', { requestId, cabinetNo: this.currentBox.cabinetNo || null, selector }) + this.debugSubmit('refresh.start', { requestId, selector }) this.remoteLoading = true this.remoteError = '' try { const warehouseId = this.currentBox.warehouseId || session.currentUser?.warehouseId || session.currentUser?.warehouse const task = await getContainerPickingTask(selector, warehouseId) if (requestId !== this.taskRequestId) return - if (!task) throw new Error('未找到当前柜次的拣货任务') + if (!task) 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.version === null || task.version === undefined) throw new Error('任务状态尚未同步,请刷新重试') + if (!task.currentContainerNo) throw new Error('任务尚未同步集装箱号,请返回任务列表刷新') const remoteItems = task.items || task.pickItems || task.details || task.lines - if (Array.isArray(remoteItems) && remoteItems.length) { + if (!Array.isArray(remoteItems) || !remoteItems.length) throw new Error('当前拣货任务暂无可作业明细,请返回刷新') + { this.currentBox.items = remoteItems.map((item) => ({ ...item, id: item.id ?? item.itemId, @@ -873,10 +906,11 @@ 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 || firstItem.inboundCode || this.currentBox.orderNo - if (!firstItem.inboundId) throw new Error('服务端任务明细缺少 inboundId,无法进入装柜取证') - this.currentBox.inboundId = firstItem.inboundId - this.currentBox.tenantId = firstItem.tenantId || null + this.currentBox.inboundId = task.inboundId + this.currentBox.tenantId = evidenceItem.tenantId || null const serverStates = createItemStates(this.currentBox.items) const serverPartialQuantities = {} this.currentBox.items.forEach((item) => { @@ -890,28 +924,35 @@ this.partialQuantities = serverPartialQuantities this.scannedBoxCodes = {} } - this.remoteTaskId = task.taskId ?? task.id + this.remoteTaskId = detailTaskId if (!this.remoteTaskId) throw new Error('服务端任务缺少 taskId,无法开始拣货') - this.assignmentVersion = task.assignmentVersion ?? task.version ?? 0 this.currentBox.loadingPlanId = task.loadingPlanId this.currentBox.containerBookingId = task.containerBookingId this.currentBox.cabinetNo = task.cabinetNo - this.currentBox.boxNo = task.cabinetNo + this.currentBox.boxNo = task.currentContainerNo this.currentBox.currentContainerNo = task.currentContainerNo const remoteTask = normalizePickingTask(task) + this.assignmentVersion = remoteTask.assignment?.version ?? remoteTask.assignmentVersion ?? (remoteTask.workflowStatus === 'UNASSIGNED' ? 0 : null) + if (this.assignmentVersion === null) throw new Error('任务领取状态尚未同步,请刷新重试') let claimed = null - if (!isPickingCompleteTask(remoteTask)) { + if (remoteTask.workflowStatus === 'UNASSIGNED') { const operationId = `claim-${session.currentUser?.id || 'operator'}-${this.remoteTaskId}-${task.loadingPlanId}-${task.containerBookingId}` claimed = await claimContainerPickingTask(this.remoteTaskId, this.assignmentVersion, operationId) if (requestId !== this.taskRequestId) return + } else if (remoteTask.workflowStatus === 'IN_PROGRESS') { + const assigneeId = remoteTask.assignment?.assigneeId + if (!assigneeId || String(assigneeId) !== String(session.currentUser?.id || '')) throw new Error('当前任务已由其他作业员领取,请返回任务列表刷新') + } else if (!isPickingCompleteTask(remoteTask)) { + throw new Error('当前任务尚未进入可拣货状态,请返回任务列表刷新') } 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 = task.inboundVersion ?? task.loadingVersion ?? this.remotePick.inboundVersion ?? this.remotePick.loadingVersion ?? null + this.loadingVersion = this.remotePick.inboundVersion ?? this.remotePick.loadingVersion ?? null this.evidenceIdentity = this.captureEvidenceIdentity() this.restoreEvidenceDraft() this.debugSubmit('refresh.ready', { requestId, workflowStatus: this.remotePick.workflowStatus, taskStatus: this.remotePick.taskStatus, assignmentVersion: this.assignmentVersion, loadingVersion: this.loadingVersion, evidenceIdentity: this.evidenceIdentity }) - this.assignmentVersion = this.remotePick.assignmentVersion ?? this.assignmentVersion + this.assignmentVersion = this.remotePick.assignment?.version ?? this.remotePick.assignmentVersion ?? this.assignmentVersion if (!this.pickingCompleted) this.restoreRemoteDraft() if (this.loadingStageReady) await this.loadLoadingEvidence() } catch (error) { @@ -1133,6 +1174,7 @@ this.persistSession() }, pickItem(id, source = 'manual') { + if (!this.requirePickingOperation()) return const item = this.currentBox.items.find((entry) => entry.id === id) const available = this.availablePickQuantity(item) if (available !== null && available < Number(item.qty || 0)) { this.showToast(`库存/库位可用量不足:可拣 ${available}`, 'error'); return } @@ -1146,6 +1188,7 @@ this.scanHistory = [entry, ...this.scanHistory].slice(0, 3) }, undoItem(id) { + if (!this.requirePickingOperation()) return const item = this.currentBox.items.find((entry) => entry.id === id) if (item) item.skipReason = '' this.itemStates = { ...this.itemStates, [id]: ITEM_STATUS.PENDING } @@ -1158,11 +1201,13 @@ this.persistSession() }, openSkip(id) { + if (!this.requirePickingOperation()) return this.skipTargetId = id this.selectedSkipReason = '' this.openOverlay('skip') }, confirmSkip() { + if (!this.requirePickingOperation()) return if (!this.selectedSkipReason) { this.showToast('请先选择跳拣原因', 'warning') return @@ -1176,6 +1221,7 @@ this.persistSession() }, openPartial(id) { + if (!this.requirePickingOperation()) return this.partialTargetId = id this.partialInput = '' this.openOverlay('partial') @@ -1191,6 +1237,7 @@ }) }, confirmPartial() { + if (!this.requirePickingOperation()) return const item = this.partialTargetItem if (!item) return if (!this.partialInputValid) return @@ -1232,6 +1279,7 @@ this.requestBoxSwitch(entry.index) }, submitPicking() { + if (!this.requirePickingOperation()) return if (!this.summary.canSubmit || this.completedBoxes[this.currentBoxIndex] || this.pickingCompleted) return if (this.summary.pending > 0) { this.selectedIncompleteReason = '' diff --git a/pages/tasks/tasks.vue b/pages/tasks/tasks.vue index 4fccc47..efb8a06 100644 --- a/pages/tasks/tasks.vue +++ b/pages/tasks/tasks.vue @@ -8,10 +8,11 @@ {{ error }} 正在加载真实任务... - 暂无待处理拣货任务 + 暂无可领取的拣货任务。请确认预装已补充集装箱号、已关联放舱订舱且预装已确认。 + {{ notice }} - {{ task.cabinetNo || '待分配柜次' }}{{ statusLabel(task.workflowStatus) }} - 当前真实柜号 {{ task.currentContainerNo }} + 集装箱号 {{ task.currentContainerNo }}{{ statusLabel(task.workflowStatus) }} + 柜次号 {{ task.cabinetNo }} {{ task.displayNo || '未命名订单' }} {{ task.warehouseName || (task.warehouseId ? `仓库 ${task.warehouseId}` : '缺少仓库信息') }} · 待处理 {{ task.pendingItemCount ?? task.itemCount ?? 0 }} 项 @@ -29,26 +30,28 @@ export default { components: { PortalHeader }, - data() { return { user: session.currentUser, tasks: [], loading: false, error: '', online: true, toast: { visible: false, message: '', type: '' } } }, + data() { return { user: session.currentUser, tasks: [], loading: false, error: '', notice: '', online: true, toast: { visible: false, message: '', type: '' } } }, onLoad() { if (!requireSession()) return; this._stopNetwork = watchNetwork((value) => { this.online = value }) }, onShow() { if (!requireSession()) return; this.user = session.currentUser; this.loadTasks() }, 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.warehouseId && task.taskStatus === 'LOADING' && ['UNASSIGNED', 'IN_PROGRESS'].includes(task.workflowStatus)) }, + 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)) }, async loadTasks() { if (!isOnline()) { this.error = '当前网络不可用,请联网后刷新'; return } - this.loading = true; this.error = '' + this.loading = true; this.error = ''; this.notice = '' try { const result = await listLoadingOrders({ page: 1, size: 50 }) - this.tasks = (result.records || []).filter((task) => task.taskId) + const records = result.records || [] + this.tasks = records.filter((task) => this.isTaskReady(task)) + if (records.length > this.tasks.length) this.notice = '部分任务尚未完成服务端就绪校验,本机已隐藏,请稍后刷新' } catch (error) { this.error = error.network ? '当前网络不可用,请联网后刷新' : (error.message || '任务加载失败') } finally { this.loading = false } }, enterTask(task) { if (!this.isTaskReady(task)) { this.showToast('任务尚未满足后端进入条件', 'error'); return } - uni.navigateTo({ url: `/pages/picking/picking?cabinetNo=${encodeURIComponent(task.cabinetNo)}¤tContainerNo=${encodeURIComponent(task.currentContainerNo || '')}&loadingPlanId=${encodeURIComponent(task.loadingPlanId)}&containerBookingId=${encodeURIComponent(task.containerBookingId)}&warehouseId=${encodeURIComponent(task.warehouseId)}&taskId=${encodeURIComponent(task.taskId)}&orderNo=${encodeURIComponent(task.displayNo || '')}` }) + uni.navigateTo({ url: `/pages/picking/picking?cabinetNo=${encodeURIComponent(task.cabinetNo)}¤tContainerNo=${encodeURIComponent(task.currentContainerNo)}&warehouseId=${encodeURIComponent(task.warehouseId)}&taskId=${encodeURIComponent(task.taskId)}&orderNo=${encodeURIComponent(task.displayNo || '')}` }) }, showToast(message, type) { clearTimeout(this._toastTimer); this.toast = { visible: true, message, type }; this._toastTimer = setTimeout(() => { this.toast.visible = false }, 2200) } } @@ -63,6 +66,7 @@ .task-refresh[disabled] { opacity: .6; } .task-error, .task-state { padding: 24px 16px; border-radius: 12px; background: #fff; color: #667085; text-align: center; } .task-error { color: #b42318; background: #fff1f0; } +.task-notice { padding: 12px 14px; margin-bottom: 12px; border-left: 4px solid #f79009; background: #fffaeb; color: #7a2e0e; font-size: 14px; } .task-card { padding: 16px; margin-bottom: 12px; border: 1px solid #e3eaf3; border-radius: 12px; background: #fff; box-shadow: 0 4px 16px rgba(23, 55, 93, .06); } .task-card-head { display: flex; align-items: center; justify-content: space-between; } .task-code { color: #12233f; font-weight: 700; } diff --git a/tests/page-contract.test.mjs b/tests/page-contract.test.mjs index 685d2e9..b43072c 100644 --- a/tests/page-contract.test.mjs +++ b/tests/page-contract.test.mjs @@ -43,6 +43,12 @@ test('picking task list uses remote loading orders and never falls back to demo assert.match(tasks, /listLoadingOrders/) assert.match(tasks, /当前网络不可用,请联网后刷新/) assert.match(tasks, /进入并开始拣货/) + assert.match(tasks, /暂无可领取的拣货任务/) + assert.match(tasks, /集装箱号 \{\{ task\.currentContainerNo \}\}/) + assert.match(tasks, /柜次号 \{\{ task\.cabinetNo \}\}/) + assert.match(tasks, /records\.filter\(\(task\) => this\.isTaskReady\(task\)\)/) + 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_/) assert.match(home, /pages\/tasks\/tasks/) assert.match(modulePage, /pages\/tasks\/tasks/) @@ -105,7 +111,7 @@ test('picking page keeps scan, draft and risky transition contracts', async () = assert.match(page, / { assert.match(source, /\/first-leg\/pda-loading\/tasks\/pending/) - assert.match(source, /\/first-leg\/pda-loading\/tasks\?/) + assert.doesNotMatch(source, /query\.push\(`cabinetNo=/) + assert.doesNotMatch(source, /query\.push\(`loadingPlanId=/) assert.match(source, /tasks\/\$\{encodeURIComponent\(taskId\)\}\/claim/) assert.match(source, /tasks\/\$\{encodeURIComponent\(task\.taskId\)\}\/batch/) assert.match(source, /tasks\/\$\{encodeURIComponent\(taskId\)\}\/inventory-snapshot\?warehouseId=/) @@ -92,8 +93,6 @@ test('PDA cabinet picking adapter claims online and submits one atomic batch', ( assert.match(page, /submitContainerPickingBatch\(/) assert.match(page, /任务版本已变化,请刷新后重试/) assert.match(source, /data: \{ warehouseId, loadingPlanId: task\.loadingPlanId, containerBookingId: task\.containerBookingId, expectedVersion, inventorySnapshotToken, items \}/) - assert.match(source, /cabinetNo=/) - assert.match(source, /loadingPlanId=/) assert.doesNotMatch(source, /query\.push\(`containerNo=/) assert.doesNotMatch(page, /getContainerPickingTask\([^\n]*containerNo:/) assert.match(source, /tasks\/\$\{encodeURIComponent\(taskId\)\}\?warehouseId=/) @@ -110,7 +109,8 @@ test('PDA cabinet picking adapter claims online and submits one atomic batch', ( assert.match(page, /remoteError && !pickingCompleted/) assert.match(page, /库存快照未同步,请刷新任务后重试/) assert.match(page, /拣货完成未提交,请查看错误提示/) - assert.match(page, /if \(!isPickingCompleteTask\(remoteTask\)\)[\s\S]*claimContainerPickingTask/) + assert.match(page, /if \(remoteTask\.workflowStatus === 'UNASSIGNED'\)[\s\S]*claimContainerPickingTask/) + assert.match(page, /this\.loadingVersion = this\.remotePick\.inboundVersion \?\? this\.remotePick\.loadingVersion/) assert.match(page, /taskRequestId/) assert.match(page, /resetEvidenceState/) assert.match(page, /availablePickQuantity/) diff --git a/tests/picking-evidence-stage.test.mjs b/tests/picking-evidence-stage.test.mjs index e2d5526..2375828 100644 --- a/tests/picking-evidence-stage.test.mjs +++ b/tests/picking-evidence-stage.test.mjs @@ -49,8 +49,10 @@ test('picking and evidence are separate operator-selected stages', () => { test('evidence identity is bound to task, inbound, loading plan and container booking', () => { assert.match(page, /captureEvidenceIdentity\(\)[\s\S]*?taskId:[\s\S]*?inboundId:[\s\S]*?loadingPlanId:[\s\S]*?containerBookingId:/) assert.match(page, /isSameEvidenceIdentity\(left, right\)[\s\S]*?taskId[\s\S]*?inboundId[\s\S]*?loadingPlanId[\s\S]*?containerBookingId/) - assert.match(page, /this\.currentBox\.inboundId = firstItem\.inboundId/) - assert.doesNotMatch(page, /firstItem\.inboundId \|\| this\.currentBox\.inboundId/) + assert.match(page, /this\.currentBox\.inboundId = task\.inboundId/) + assert.match(page, /String\(detailTaskId \|\| ''\) !== String\(requestedTaskId\)/) + assert.match(page, /!task\.inboundId \|\| !task\.loadingPlanId \|\| !task\.containerBookingId/) + assert.match(page, /evidenceItem[\s\S]*?String\(task\.inboundId\)/) assert.match(page, /this\.evidenceIdentity = this\.captureEvidenceIdentity\(\)/) }) @@ -114,6 +116,15 @@ test('returning from the camera does not refresh and invalidate the active evide assert.match(chooser, /fail: \(error\) => \{[\s\S]*?this\.cameraPicking = false/) }) +test('returning from an image preview does not refresh the picking task', () => { + assert.match(page, /evidencePreviewOpening: false, evidencePreviewReturning: false/) + assert.match(page, /onShow\(\) \{[\s\S]*?if \(this\.evidencePreviewReturning\)[\s\S]*?preview\.resume[\s\S]*?return[\s\S]*?this\.syncRemoteTask\(\)/) + assert.match(page, /onHide\(\) \{[\s\S]*?if \(this\.evidencePreviewOpening\)[\s\S]*?this\.evidencePreviewReturning = true/) + const preview = page.match(/previewEvidencePhoto\(photo, type\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || '' + assert.match(preview, /this\.evidencePreviewOpening = true/) + assert.match(preview, /uni\.previewImage\(\{ current, urls, fail:/) +}) + test('evidence save, void and loading completion ignore stale cabinet responses', () => { for (const [method, awaitedCall] of [ ['saveEvidence', 'upsertEvidence'], @@ -132,6 +143,16 @@ test('evidence save, void and loading completion ignore stale cabinet responses' test('completed server tasks restore evidence stage without being claimed again', () => { assert.match(page, /const remoteTask = normalizePickingTask\(task\)/) - assert.match(page, /if \(!isPickingCompleteTask\(remoteTask\)\) \{[\s\S]*?claimContainerPickingTask/) + assert.match(page, /if \(remoteTask\.workflowStatus === 'UNASSIGNED'\) \{[\s\S]*?claimContainerPickingTask/) + assert.match(page, /else if \(!isPickingCompleteTask\(remoteTask\)\)/) assert.match(page, /if \(this\.loadingStageReady\) await this\.loadLoadingEvidence\(\)/) }) + +test('picking mutations stay closed until the canonical task has been claimed', () => { + assert.match(page, /pickingOperationReady\(\)[\s\S]*?workflowStatus === 'IN_PROGRESS'[\s\S]*?remoteTaskId[\s\S]*?inboundId[\s\S]*?loadingPlanId[\s\S]*?containerBookingId/) + assert.match(page, /requirePickingOperation\(\)[\s\S]*?任务尚未领取完成/) + for (const method of ['processScannedValue', 'pickItem', 'openSkip', 'openPartial', 'submitPicking']) { + const body = page.match(new RegExp(`${method}\\([^)]*\\) \\{[\\s\\S]*?\\n\\t\\t\\t\\},`))?.[0] || '' + assert.match(body, /if \(!this\.requirePickingOperation\(\)\) return/, `${method} should wait for claim`) + } +})