feat: gate PDA loading evidence by ready task
This commit is contained in:
+7
-18
@@ -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
@@ -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
@@ -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)}¤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; }
|
||||
|
||||
@@ -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\)/)
|
||||
|
||||
@@ -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/)
|
||||
|
||||
@@ -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`)
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user