feat(pda): connect online picking workflow

This commit is contained in:
2026-08-11 11:31:05 +08:00
parent fc1944fb7e
commit be776c8efa
16 changed files with 440 additions and 82 deletions
+40 -18
View File
@@ -1,4 +1,5 @@
import { session } from '@/stores/session.js'
import { requireOnline } from '@/utils/network.js'
const EVIDENCE_TYPES = Object.freeze([
'PHOTO_STEP_1', 'PHOTO_STEP_2', 'PHOTO_STEP_3', 'PHOTO_STEP_4', 'SEAL'
@@ -19,6 +20,7 @@ const EVIDENCE_SLOTS = Object.freeze([
// server success when any required identifier or version is absent.
function request(options) {
requireOnline()
const baseUrl = session.apiBase || '/api/platform/v1'
const headers = { ...(options.header || {}) }
if (session.token) headers.Authorization = `Bearer ${session.token}`
@@ -57,7 +59,14 @@ function idempotencyKey(operation, inboundId) {
}
function stableIdempotencyKey(operationId, suffix = '') {
return `pda-${operationId}${suffix ? `-${suffix}` : ''}`
const raw = `pda-${operationId}${suffix ? `-${suffix}` : ''}`
let hash = 2166136261
for (let index = 0; index < raw.length; index += 1) {
hash ^= raw.charCodeAt(index)
hash = Math.imul(hash, 16777619)
}
const prefix = raw.replace(/[^A-Za-z0-9._:-]/g, '-').slice(0, 96)
return `${prefix}-${(hash >>> 0).toString(36)}-${raw.length}`.slice(0, 128)
}
function tenantQuery(tenantId) {
@@ -67,14 +76,14 @@ function tenantQuery(tenantId) {
}
export function isRemoteReady(inboundId) {
return Boolean(session.token && session.tenantId && inboundId)
return Boolean(session.token && (session.tenantId || inboundId))
}
export function getContainerPickingTask(containerNo, warehouseId) {
if (!containerNo) throw new Error('缺少柜次号,无法请求现场任务')
const query = [`containerNo=${encodeURIComponent(containerNo)}`]
if (warehouseId) query.push(`warehouseId=${encodeURIComponent(warehouseId)}`)
query.push(`tenantId=${encodeURIComponent(tenantQuery())}`)
if (session.tenantId) query.push(`tenantId=${encodeURIComponent(session.tenantId)}`)
return request({ url: `/first-leg/pda-loading/tasks?${query.join('&')}`, method: 'GET' }).then((data) => {
if (Array.isArray(data)) return data[0] || null
if (data?.records) return data.records[0] || null
@@ -118,20 +127,26 @@ export function skipContainerItem(taskId, itemId, payload = {}) {
export async function submitContainerPickingBatch(task, actions, { warehouseId, operationId } = {}) {
if (!task?.taskId) throw new Error('缺少拣货任务标识,无法提交')
const submitOperationId = operationId || `submit-${task.taskId}-${task.containerNo || ''}-${task.version ?? task.pickingVersion ?? ''}`
const results = []
let version = task.version ?? task.pickingVersion
for (const action of actions || []) {
const common = { warehouseId, locationIds: action.locationIds || [], version: action.version ?? version, operationId: `${submitOperationId}-${action.itemId}` }
let result
if (action.type === 'PICK') result = await pickContainerItem(task.taskId, action.itemId, { ...common, pickedPackageCount: action.pickedPackageCount })
else if (action.type === 'PARTIAL') result = await partialContainerItem(task.taskId, action.itemId, { ...common, finalPickedPackageCount: action.finalPickedPackageCount })
else if (action.type === 'SKIP') result = await skipContainerItem(task.taskId, action.itemId, { ...common, reason: action.reason })
if (result !== undefined) {
results.push(result)
version = result?.version ?? result?.taskVersion ?? result?.pickingVersion ?? version
}
}
return results
const expectedVersion = task.version ?? task.pickingVersion
if (expectedVersion == null) throw new Error('缺少拣货任务版本,无法整批提交')
if (!warehouseId) throw new Error('缺少作业仓库标识,无法整批提交')
if (!task.containerNo) throw new Error('缺少柜号,无法整批提交')
const items = (actions || []).map((action) => ({
itemId: action.itemId,
action: action.type,
pickedPackageCount: action.type === 'PICK' ? action.pickedPackageCount : null,
finalPickedPackageCount: action.type === 'PARTIAL' ? action.finalPickedPackageCount : null,
releaseMode: action.type === 'SKIP' ? null : (action.releaseMode || 'AUTO'),
locationIds: action.type === 'SKIP' ? [] : (action.locationIds || []),
version: action.version,
reason: action.type === 'SKIP' ? action.reason : null
}))
return request({
url: `/first-leg/pda-loading/tasks/${encodeURIComponent(task.taskId)}/batch`,
method: 'POST',
header: { 'Idempotency-Key': stableIdempotencyKey(submitOperationId) },
data: { warehouseId, containerNo: task.containerNo, expectedVersion, items }
})
}
export function listLoadingOrders({ page = 1, size = 50, keyword = '' } = {}) {
@@ -150,7 +165,13 @@ export function normalizePdaOrder(row = {}) {
...row,
inboundId: row.inboundId ?? row.inbound?.id ?? null,
inboundVersion: row.inboundVersion ?? row.inbound?.version ?? null,
pickingVersion: row.pickingVersion ?? row.loading?.pickingVersion ?? null
tenantId: row.tenantId ?? row.accountId ?? row.inbound?.tenantId ?? null,
tenantId: row.tenantId ?? row.accountId ?? row.inbound?.tenantId ?? null,
pickingVersion: row.pickingVersion ?? row.loading?.pickingVersion ?? null,
taskId: row.taskId ?? row.pdaTaskId ?? row.loadingTaskId ?? row.loading?.taskId ?? null,
containerNo: row.containerNo ?? row.container?.containerNo ?? row.loadingPlan?.containerNo ?? row.loading?.containerNo ?? '',
warehouseId: row.warehouseId ?? row.warehouse?.id ?? row.loadingPlan?.warehouseId ?? null,
displayNo: row.orderNo ?? row.inboundCode ?? row.order?.orderNo ?? row.id
}
}
@@ -218,6 +239,7 @@ export function voidEvidence(inboundId, evidenceId, payload = {}, tenantId) {
/** Upload a local photo to the file service and return its durable fileId. */
export function uploadEvidenceFile(inboundId, evidenceType, localPath, tenantId, onProgress) {
requireOnline('当前网络不可用,无法上传装柜照片')
if (!EVIDENCE_TYPES.includes(evidenceType)) throw new Error('不支持的装柜证据类型')
if (!localPath) throw new Error('缺少待上传的照片')
const tenant = tenantQuery(tenantId)