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`)
+ }
+})