feat: gate PDA loading evidence by ready task

This commit is contained in:
2026-08-15 17:04:00 +08:00
parent 207c872dff
commit 3bb5da8ae7
6 changed files with 147 additions and 79 deletions
+7 -18
View File
@@ -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 ?? '业务单号待同步'
}
}
+92 -44
View File
@@ -14,18 +14,18 @@
<view class="work-content">
<view class="box-select-wrap">
<button class="box-select-btn" @tap="toggleBoxDropdown">
<text>{{ currentBox.boxNo }}<template v-if="currentBox.currentContainerNo"> · 当前柜号 {{ currentBox.currentContainerNo }}</template> · 订单 {{ currentBox.orderNo }}</text>
<text>集装箱号 {{ currentBox.currentContainerNo || '同步中' }} · 柜次号 {{ currentBox.cabinetNo || '同步中' }} · 订单 {{ currentBox.orderNo }}</text>
<text class="box-chevron" :class="{ open: boxDropdownOpen }">▼</text>
</button>
<view v-if="boxDropdownOpen" class="box-dropdown">
<view
v-for="(box, index) in boxes"
:key="box.boxNo"
:key="box.taskId || box.cabinetNo"
class="box-option"
:class="{ active: index === currentBoxIndex, completed: completedBoxes[index] }"
@tap="requestBoxSwitch(index)"
>
<view><text class="box-option-number">{{ box.boxNo }}</text><text class="box-option-order">{{ box.orderNo }}</text></view>
<view><text class="box-option-number">{{ box.currentContainerNo || '集装箱号同步中' }}</text><text class="box-option-order">柜次号 {{ box.cabinetNo || '同步中' }} · {{ box.orderNo }}</text></view>
<text v-if="completedBoxes[index]" class="box-option-state">已完成</text>
<text v-else-if="hasBoxDraft(index)" class="box-option-state draft">有草稿</text>
</view>
@@ -70,7 +70,7 @@
<text> / {{ summary.total }} 箱</text>
</template>
</view>
<button class="box-tasks-btn" @tap="openBoxList('pending')">箱任务 {{ completedCount }}/{{ boxes.length }}</button>
<button class="box-tasks-btn" @tap="openBoxList('pending')">柜次任务 {{ completedCount }}/{{ boxes.length }}</button>
</view>
</view>
<view class="progress-bar-bg">
@@ -81,7 +81,7 @@
<view class="pick-list">
<view v-if="orderedItems.length === 0" class="empty-state">
<text class="empty-state-icon">□</text>
<text class="empty-state-text">该箱号暂无拣货任务</text>
<text class="empty-state-text">当前集装箱暂无拣货明细</text>
</view>
<view
v-for="item in orderedItems"
@@ -297,7 +297,7 @@
<view class="complete-modal" role="dialog" aria-modal="true" aria-label="拣货完成" @tap.stop>
<PopupHeader title="拣货完成" @close="closeOverlay" />
<view class="complete-icon">✓</view>
<text class="complete-desc">箱号 {{ currentBox.boxNo }} · 订单 {{ currentBox.orderNo }}</text>
<text class="complete-desc">集装箱号 {{ currentBox.currentContainerNo }} · 柜次号 {{ currentBox.cabinetNo }} · 订单 {{ currentBox.orderNo }}</text>
<view class="complete-stats">
<view class="complete-stat">
<text class="complete-stat-num green">{{ completionSummary.picked }}</text>
@@ -324,10 +324,10 @@
</uni-popup>
<uni-popup ref="allCompletePopup" type="center" :is-mask-click="true" @change="handleOverlayChange('allComplete', $event)">
<view class="complete-modal" role="dialog" aria-modal="true" aria-label="全部箱号已完成" @tap.stop>
<PopupHeader title="全部箱号已完成" @close="closeOverlay" />
<view class="complete-modal" role="dialog" aria-modal="true" aria-label="全部柜次已完成" @tap.stop>
<PopupHeader title="全部柜次已完成" @close="closeOverlay" />
<view class="complete-icon">✓</view>
<text class="complete-desc">本班次演示任务 {{ boxes.length }} 箱已全部处理</text>
<text class="complete-desc">本班次 {{ boxes.length }} 个柜次任务已全部处理</text>
<button class="all-complete-btn" @tap="leavePicking">返回工作台</button>
</view>
</uni-popup>
@@ -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 = ''
+12 -8
View File
@@ -8,10 +8,11 @@
</view>
<view v-if="error" class="task-error" role="alert">{{ error }}</view>
<view v-if="loading" class="task-state">正在加载真实任务...</view>
<view v-else-if="!tasks.length && !error" class="task-state">暂无待处理拣货任务</view>
<view v-else-if="!tasks.length && !error" class="task-state">暂无可领取的拣货任务。请确认预装已补充集装箱号、已关联放舱订舱且预装已确认。</view>
<view v-if="notice" class="task-notice" role="status">{{ notice }}</view>
<view v-for="task in tasks" :key="task.taskId" class="task-card">
<view class="task-card-head"><text class="task-code">{{ task.cabinetNo || '待分配柜次' }}</text><text class="task-status">{{ statusLabel(task.workflowStatus) }}</text></view>
<text v-if="task.currentContainerNo" class="task-order">当前真实柜号 {{ task.currentContainerNo }}</text>
<view class="task-card-head"><text class="task-code">集装箱号 {{ task.currentContainerNo }}</text><text class="task-status">{{ statusLabel(task.workflowStatus) }}</text></view>
<text class="task-order">柜次号 {{ task.cabinetNo }}</text>
<text class="task-order">{{ task.displayNo || '未命名订单' }}</text>
<text class="task-meta">{{ task.warehouseName || (task.warehouseId ? `仓库 ${task.warehouseId}` : '缺少仓库信息') }} · 待处理 {{ task.pendingItemCount ?? task.itemCount ?? 0 }} 项</text>
<button class="task-enter" :disabled="!isTaskReady(task)" @tap="enterTask(task)">{{ isTaskReady(task) ? '进入并开始拣货' : '任务暂不可进入' }}</button>
@@ -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)}&currentContainerNo=${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)}&currentContainerNo=${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; }
+7 -1
View File
@@ -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, /<ScanTargetInput v-model="scanValue" name="boxCode"[\s\S]*?:active="activeScanField === 'boxCode'"[\s\S]*?@activate="setScanTarget"[\s\S]*?@submit="handleManualSubmit"/)
assert.match(page, /beforeUnmount\(\)/)
assert.match(page, /startScanBroadcast/)
assert.match(page, /onHide\(\) \{ this\.stopHardwareScanner\(\) \}/)
assert.match(page, /onHide\(\) \{[\s\S]*?this\.stopHardwareScanner\(\)[\s\S]*?\}/)
assert.match(page, /handleHardwareScan\(value\)[\s\S]*?this\.scanValue = value[\s\S]*?this\.processScannedValue\(value, 'broadcast'\)/)
assert.match(page, /handleManualSubmit\(value\)[\s\S]*?\.trim\(\)\.toUpperCase\(\)[\s\S]*?this\.processScannedValue\(normalizedValue, 'manual'\)/)
assert.match(page, /recordScanResult\(type, title, boxCode, detail\)/)
+5 -5
View File
@@ -41,7 +41,7 @@ test('C02 identifiers and separate picking/inbound versions are normalized expli
assert.match(page, /item\.itemStatus === 'PICKED'/)
assert.match(page, /item\.itemStatus === 'PARTIAL'/)
assert.match(page, /pickingCompleted \|\| !summary\.canSubmit/)
assert.match(page, /this\.loadingVersion = task\.inboundVersion \?\? task\.loadingVersion/)
assert.match(page, /this\.loadingVersion = this\.remotePick\.inboundVersion \?\? this\.remotePick\.loadingVersion/)
assert.match(page, /状态尚未同步,请刷新重试/)
})
@@ -80,7 +80,8 @@ test('PDA downloads authenticated evidence previews returned by the backend', ()
test('PDA cabinet picking adapter claims online and submits one atomic batch', () => {
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/)
+24 -3
View File
@@ -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`)
}
})