2026-08-06 13:56:01 +08:00
import { session } from '@/stores/session.js'
2026-08-11 11:31:05 +08:00
import { requireOnline } from '@/utils/network.js'
2026-08-06 13:56:01 +08:00
const EVIDENCE_TYPES = Object . freeze ([
'PHOTO_STEP_1' , 'PHOTO_STEP_2' , 'PHOTO_STEP_3' , 'PHOTO_STEP_4' , 'SEAL'
])
2026-08-10 10:53:11 +08:00
// Keep the confirmed business labels in the PDA while the server retains its
// legacy evidence codes. Remove this mapping only after the backend contract is migrated.
const EVIDENCE_SLOTS = Object . freeze ([
{ type : 'PHOTO_STEP_1' , label : '空仓照片' },
{ type : 'PHOTO_STEP_2' , label : '半仓照片' },
{ type : 'PHOTO_STEP_3' , label : '满仓照片' },
{ type : 'PHOTO_STEP_4' , label : '锁仓照片' },
{ type : 'SEAL' , label : '带锁码照' }
])
2026-08-06 13:56:01 +08:00
// C02 supplies inboundId/inboundVersion and loading.pickingVersion. C04 supplies
// inbound-scoped task and evidence operations. No local task is promoted to a
// server success when any required identifier or version is absent.
2026-08-15 09:47:28 +08:00
function debugLog ( event , payload = {}) {
try { console . info ( `[PDA] ${ event } ` , payload ) } catch ( _ ) { /* console may be unavailable on a device */ }
}
2026-08-06 13:56:01 +08:00
function request ( options ) {
2026-08-11 11:31:05 +08:00
requireOnline ()
2026-08-06 13:56:01 +08:00
const baseUrl = session . apiBase || '/api/platform/v1'
const headers = { ...( options . header || {}) }
if ( session . token ) headers . Authorization = `Bearer ${ session . token } `
if ( session . requestId ) headers [ 'X-Request-Id' ] = session . requestId
return new Promise (( resolve , reject ) => {
uni . request ({
... options ,
url : ` ${ baseUrl }${ options . url } ` ,
header : headers ,
success ( response ) {
const body = response . data || {}
const code = String ( body . code ?? response . statusCode )
2026-08-15 11:36:56 +08:00
2026-08-06 13:56:01 +08:00
if ( code !== '200' ) {
const error = new Error ( body . message || '服务端请求失败' )
error . code = body . code || response . statusCode
error . statusCode = response . statusCode
2026-08-10 10:53:11 +08:00
error . requestId = body . requestId || response . header ? .[ 'X-Request-Id' ] || response . header ? .[ 'x-request-id' ]
error . traceId = body . traceId || response . header ? .[ 'X-Trace-Id' ] || response . header ? .[ 'x-trace-id' ]
2026-08-06 13:56:01 +08:00
reject ( error )
return
}
resolve ( body . data )
},
fail ( error ) {
const next = new Error ( error . errMsg || '网络连接失败' )
next . network = true
reject ( next )
}
})
})
}
function idempotencyKey ( operation , inboundId ) {
return `pda- ${ operation } - ${ inboundId } - ${ Date . now () } - ${ Math . random (). toString ( 36 ). slice ( 2 , 8 ) } `
}
2026-08-10 10:53:11 +08:00
function stableIdempotencyKey ( operationId , suffix = '' ) {
2026-08-11 11:31:05 +08:00
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 )
2026-08-10 10:53:11 +08:00
}
2026-08-06 13:56:01 +08:00
function tenantQuery ( tenantId ) {
const value = tenantId ?? session . tenantId
if ( ! value ) throw new Error ( '缺少客户隔离标识 tenantId,无法请求现场任务' )
return String ( value )
}
export function isRemoteReady ( inboundId ) {
2026-08-11 11:31:05 +08:00
return Boolean ( session . token && ( session . tenantId || inboundId ))
2026-08-06 13:56:01 +08:00
}
2026-08-12 18:32:31 +08:00
export function getContainerPickingTask ( identity , warehouseId ) {
2026-08-15 09:47:28 +08:00
const selector = typeof identity === 'object' && identity !== null ? identity : {}
2026-08-12 18:32:31 +08:00
const taskId = selector . taskId ?? selector . id
2026-08-15 17:04:00 +08:00
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 )
2026-08-10 10:53:11 +08:00
}
export function claimContainerPickingTask ( taskId , expectedAssignmentVersion , operationId ) {
if ( ! taskId ) throw new Error ( '缺少拣货任务标识,无法开始作业' )
return request ({
url : `/first-leg/pda-loading/tasks/ ${ encodeURIComponent ( taskId ) } /claim` ,
method : 'POST' ,
header : { 'Idempotency-Key' : stableIdempotencyKey ( operationId || `claim- ${ taskId } ` ) },
data : { expectedAssignmentVersion , operationId : operationId || `pda-claim- ${ taskId } ` }
})
}
2026-08-11 18:46:04 +08:00
export function getPickingInventorySnapshot ( taskId , warehouseId ) {
if ( ! taskId ) throw new Error ( '缺少拣货任务标识,无法查询实时库存' )
if ( ! warehouseId ) throw new Error ( '缺少作业仓库标识,无法查询实时库存' )
2026-08-12 18:32:31 +08:00
return request ({
url : `/first-leg/pda-loading/tasks/ ${ encodeURIComponent ( taskId ) } /inventory-snapshot?warehouseId= ${ encodeURIComponent ( warehouseId ) } ` ,
method : 'GET'
}). then ( normalizeInventorySnapshot )
2026-08-11 18:46:04 +08:00
}
2026-08-10 10:53:11 +08:00
function itemAction ( taskId , itemId , path , payload , operationId ) {
// Concrete routes include /items/${encodeURIComponent(itemId)}/pick, /items/${encodeURIComponent(itemId)}/partial and /items/${encodeURIComponent(itemId)}/skip.
if ( ! taskId || itemId == null ) throw new Error ( '缺少拣货任务或明细标识,无法提交' )
return request ({
url : `/first-leg/pda-loading/tasks/ ${ encodeURIComponent ( taskId ) } /items/ ${ encodeURIComponent ( itemId ) } / ${ path } ` ,
method : 'POST' ,
header : { 'Idempotency-Key' : stableIdempotencyKey ( operationId || ` ${ path } - ${ taskId } - ${ itemId } ` ) },
data : payload
})
}
export function pickContainerItem ( taskId , itemId , payload = {}) {
return itemAction ( taskId , itemId , 'pick' , payload , payload . operationId )
}
export function partialContainerItem ( taskId , itemId , payload = {}) {
return itemAction ( taskId , itemId , 'partial' , payload , payload . operationId )
}
export function skipContainerItem ( taskId , itemId , payload = {}) {
return itemAction ( taskId , itemId , 'skip' , payload , payload . operationId )
}
2026-08-11 18:46:04 +08:00
export async function submitContainerPickingBatch ( task , actions , { warehouseId , operationId , inventorySnapshotToken } = {}) {
2026-08-12 18:32:31 +08:00
if ( ! task ? . taskId ) throw new Error ( '缺少拣货任务标识,无法提交' )
if ( ! task . loadingPlanId || ! task . containerBookingId ) throw new Error ( '缺少稳定柜次标识,无法整批提交' )
const submitOperationId = operationId || `submit- ${ task . taskId } - ${ task . loadingPlanId } - ${ task . containerBookingId } - ${ task . version ?? task . pickingVersion ?? '' } `
2026-08-11 11:31:05 +08:00
const expectedVersion = task . version ?? task . pickingVersion
if ( expectedVersion == null ) throw new Error ( '缺少拣货任务版本,无法整批提交' )
if ( ! warehouseId ) throw new Error ( '缺少作业仓库标识,无法整批提交' )
2026-08-11 18:46:04 +08:00
if ( ! inventorySnapshotToken ) throw new Error ( '缺少实时库存快照,请刷新后重试' )
2026-08-11 11:31:05 +08:00
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 ) },
2026-08-12 18:32:31 +08:00
data : { warehouseId , loadingPlanId : task . loadingPlanId , containerBookingId : task . containerBookingId , expectedVersion , inventorySnapshotToken , items }
2026-08-11 11:31:05 +08:00
})
2026-08-10 10:53:11 +08:00
}
2026-08-06 13:56:01 +08:00
export function listLoadingOrders ({ page = 1 , size = 50 , keyword = '' } = {}) {
2026-08-11 20:39:38 +08:00
const query = []
2026-08-06 13:56:01 +08:00
if ( keyword ) query . push ( `keyword= ${ encodeURIComponent ( keyword ) } ` )
2026-08-11 20:39:38 +08:00
const suffix = query . length ? `? ${ query . join ( '&' ) } ` : ''
return request ({ url : `/first-leg/pda-loading/tasks/pending ${ suffix } ` , method : 'GET' }). then (( data ) => ({
page , size , records : ( Array . isArray ( data ) ? data : data ? . records || []). map ( normalizePdaOrder )
2026-08-06 13:56:01 +08:00
}))
}
export function normalizePdaOrder ( row = {}) {
row = row || {}
return {
... row ,
inboundId : row . inboundId ?? row . inbound ? . id ?? null ,
inboundVersion : row . inboundVersion ?? row . inbound ? . version ?? null ,
2026-08-11 11:31:05 +08:00
tenantId : row . tenantId ?? row . accountId ?? row . inbound ? . tenantId ?? null ,
2026-08-12 18:32:31 +08:00
pickingVersion : row . pickingVersion ?? row . loading ? . pickingVersion ?? null ,
taskId : row . taskId ?? row . pdaTaskId ?? row . loadingTaskId ?? row . loading ? . taskId ?? null ,
loadingPlanId : row . loadingPlanId ?? row . loading ? . loadingPlanId ?? row . loadingPlan ? . id ?? null ,
containerBookingId : row . containerBookingId ?? row . loading ? . containerBookingId ?? row . container ? . id ?? null ,
2026-08-17 13:24:58 +08:00
sourceType : row . sourceType ?? row . container ? . sourceType ?? null ,
recordStatus : row . recordStatus ?? row . container ? . recordStatus ?? null ,
mergedIntoContainerBookingId : row . mergedIntoContainerBookingId ?? row . container ? . mergedIntoContainerBookingId ?? null ,
2026-08-12 18:32:31 +08:00
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 ,
2026-08-11 11:31:05 +08:00
warehouseId : row . warehouseId ?? row . warehouse ? . id ?? row . loadingPlan ? . warehouseId ?? null ,
2026-08-15 17:04:00 +08:00
displayNo : row . orderNo ?? row . inboundCode ?? row . order ? . orderNo ?? '业务单号待同步'
2026-08-06 13:56:01 +08:00
}
}
export function normalizePickingTask ( task = {}) {
2026-08-12 18:32:31 +08:00
task = task || {}
return {
... task ,
taskId : task . taskId ?? task . id ?? null ,
loadingPlanId : task . loadingPlanId ?? task . loading ? . loadingPlanId ?? null ,
containerBookingId : task . containerBookingId ?? task . loading ? . containerBookingId ?? null ,
2026-08-17 13:24:58 +08:00
sourceType : task . sourceType ?? task . container ? . sourceType ?? null ,
recordStatus : task . recordStatus ?? task . container ? . recordStatus ?? null ,
mergedIntoContainerBookingId : task . mergedIntoContainerBookingId ?? task . container ? . mergedIntoContainerBookingId ?? null ,
2026-08-12 18:32:31 +08:00
cabinetNo : task . cabinetNo ?? task . loading ? . cabinetNo ?? task . containerNo ?? '' ,
currentContainerNo : task . currentContainerNo ?? task . containerNo ?? null ,
pickingVersion : task . pickingVersion ?? task . version ?? null ,
2026-08-06 13:56:01 +08:00
inboundVersion : task . inboundVersion ?? null
}
}
2026-08-12 18:32:31 +08:00
export function normalizeInventorySnapshot ( snapshot = {}) {
return normalizePickingTask ({ ... snapshot , version : snapshot . taskVersion ?? snapshot . version })
}
2026-08-06 13:56:01 +08:00
export function getPickingTask ( inboundId , tenantId ) {
return request ({
url : `/inbounds/ ${ encodeURIComponent ( inboundId ) } /loading/picking?tenantId= ${ encodeURIComponent ( tenantQuery ( tenantId )) } ` ,
method : 'GET'
})
}
export function startPicking ( inboundId , payload , tenantId ) {
return request ({
url : `/inbounds/ ${ encodeURIComponent ( inboundId ) } /loading/picking/start?tenantId= ${ encodeURIComponent ( tenantQuery ( tenantId )) } ` ,
method : 'POST' ,
header : { 'Idempotency-Key' : idempotencyKey ( 'start-picking' , inboundId ) },
data : payload
})
}
export function completePicking ( inboundId , payload , tenantId ) {
return request ({
url : `/inbounds/ ${ encodeURIComponent ( inboundId ) } /loading/picking/complete?tenantId= ${ encodeURIComponent ( tenantQuery ( tenantId )) } ` ,
method : 'POST' ,
header : { 'Idempotency-Key' : idempotencyKey ( 'complete-picking' , inboundId ) },
data : payload
})
}
export function getEvidence ( inboundId , tenantId ) {
return request ({
2026-08-10 11:53:43 +08:00
url : `/inbounds/ ${ encodeURIComponent ( inboundId ) } /loading/evidence/pda?tenantId= ${ encodeURIComponent ( tenantQuery ( tenantId )) } ` ,
2026-08-06 13:56:01 +08:00
method : 'GET'
})
}
2026-08-15 12:33:33 +08:00
export function getEvidencePreviewUrl ( inboundId , evidenceId , tenantId ) {
if ( ! inboundId || ! evidenceId ) return ''
return `/api/platform/v1/inbounds/ ${ encodeURIComponent ( inboundId ) } /loading/evidence/ ${ encodeURIComponent ( evidenceId ) } /content/pda?tenantId= ${ encodeURIComponent ( tenantQuery ( tenantId )) } `
}
export function downloadEvidencePreview ( previewUrl ) {
requireOnline ( '当前网络不可用,无法查看装柜照片' )
if ( ! previewUrl ) throw new Error ( '装柜照片地址尚未同步' )
const baseUrl = session . apiBase || '/api/platform/v1'
let url = String ( previewUrl )
if ( ! /^https?:\/\//i . test ( url ) && /^https?:\/\//i . test ( baseUrl )) {
const origin = baseUrl . match ( /^(https?:\/\/[^/]+)/i ) ? .[ 1 ] || ''
url = ` ${ origin }${ url . startsWith ( '/' ) ? '' : '/' }${ url } `
}
const headers = {}
if ( session . token ) headers . Authorization = `Bearer ${ session . token } `
if ( session . requestId ) headers [ 'X-Request-Id' ] = session . requestId
debugLog ( 'evidence.preview.start' , { previewPath : String ( previewUrl ), resolvedUrl : url })
return new Promise (( resolve , reject ) => {
uni . downloadFile ({
url ,
header : headers ,
success ( response ) {
const responseHeaders = response . header || {}
const contentType = responseHeaders [ 'Content-Type' ] || responseHeaders [ 'content-type' ] || ''
const requestId = responseHeaders [ 'X-Request-Id' ] || responseHeaders [ 'x-request-id' ] || ''
const traceId = responseHeaders [ 'X-Trace-Id' ] || responseHeaders [ 'x-trace-id' ] || ''
debugLog ( 'evidence.preview.response' , { previewPath : String ( previewUrl ), statusCode : response . statusCode , contentType , hasTempFilePath : Boolean ( response . tempFilePath ), requestId , traceId })
if ( response . statusCode < 200 || response . statusCode >= 300 || ! response . tempFilePath ) {
const error = new Error ( '装柜照片下载失败' )
error . statusCode = response . statusCode
error . requestId = requestId
error . traceId = traceId
reject ( error )
return
}
if ( contentType && ! /^image\//i . test ( contentType ) && ! /^application\/octet-stream/i . test ( contentType )) {
const error = new Error ( '服务端未返回有效照片' )
error . statusCode = response . statusCode
error . contentType = contentType
error . requestId = requestId
error . traceId = traceId
reject ( error )
return
}
resolve ( response . tempFilePath )
},
fail ( error ) {
debugLog ( 'evidence.preview.error' , { previewPath : String ( previewUrl ), message : error . errMsg || '' })
const next = new Error ( error . errMsg || '装柜照片下载失败' )
next . network = true
reject ( next )
}
})
})
}
2026-08-06 13:56:01 +08:00
export function upsertEvidence ( inboundId , payload , tenantId ) {
if ( ! EVIDENCE_TYPES . includes ( payload . evidenceType )) throw new Error ( '不支持的装柜证据类型' )
return request ({
2026-08-10 11:53:43 +08:00
url : `/inbounds/ ${ encodeURIComponent ( inboundId ) } /loading/evidence/pda?tenantId= ${ encodeURIComponent ( tenantQuery ( tenantId )) } ` ,
2026-08-06 13:56:01 +08:00
method : 'POST' ,
2026-08-10 11:53:43 +08:00
header : { 'Idempotency-Key' : stableIdempotencyKey ( payload . operationId || `evidence- ${ inboundId } - ${ payload . evidenceType } - ${ payload . fileId || 'seal' } ` ) },
data : payload
})
}
export function voidEvidence ( inboundId , evidenceId , payload = {}, tenantId ) {
if ( ! inboundId || ! evidenceId ) throw new Error ( '缺少装柜证据标识,无法作废' )
if ( ! String ( payload . reason || '' ). trim ()) throw new Error ( '作废原因不能为空' )
return request ({
url : `/inbounds/ ${ encodeURIComponent ( inboundId ) } /loading/evidence/ ${ encodeURIComponent ( evidenceId ) } /void/pda?tenantId= ${ encodeURIComponent ( tenantQuery ( tenantId )) } ` ,
method : 'POST' ,
header : { 'Idempotency-Key' : stableIdempotencyKey ( payload . operationId || `void-evidence- ${ inboundId } - ${ evidenceId } - ${ payload . expectedVersion ?? 0 } ` ) },
2026-08-06 13:56:01 +08:00
data : payload
})
}
2026-08-10 10:53:11 +08:00
/** Upload a local photo to the file service and return its durable fileId. */
export function uploadEvidenceFile ( inboundId , evidenceType , localPath , tenantId , onProgress ) {
2026-08-11 11:31:05 +08:00
requireOnline ( '当前网络不可用,无法上传装柜照片' )
2026-08-10 10:53:11 +08:00
if ( ! EVIDENCE_TYPES . includes ( evidenceType )) throw new Error ( '不支持的装柜证据类型' )
if ( ! localPath ) throw new Error ( '缺少待上传的照片' )
const tenant = tenantQuery ( tenantId )
const baseUrl = session . apiBase || '/api/platform/v1'
const headers = {}
if ( session . token ) headers . Authorization = `Bearer ${ session . token } `
if ( session . requestId ) headers [ 'X-Request-Id' ] = session . requestId
2026-08-15 11:36:56 +08:00
debugLog ( 'evidence.upload.start' , { inboundId , evidenceType , tenantId : tenant , hasLocalPath : Boolean ( localPath ) })
2026-08-10 10:53:11 +08:00
return new Promise (( resolve , reject ) => {
const task = uni . uploadFile ({
url : ` ${ baseUrl } /files/loading-evidence/ ${ encodeURIComponent ( tenant ) } / ${ encodeURIComponent ( inboundId ) } ?evidenceType= ${ encodeURIComponent ( evidenceType ) } ` ,
filePath : localPath ,
name : 'file' ,
header : headers ,
success ( response ) {
let body = response . data
try { body = typeof body === 'string' ? JSON . parse ( body ) : body } catch ( _ ) { /* handled below */ }
const code = String ( body ? . code ?? response . statusCode )
2026-08-15 11:36:56 +08:00
debugLog ( 'evidence.upload.response' , { inboundId , evidenceType , statusCode : response . statusCode , code , message : body ? . message || '' , requestId : body ? . requestId || response . header ? .[ 'X-Request-Id' ] || response . header ? .[ 'x-request-id' ] || '' , traceId : body ? . traceId || response . header ? .[ 'X-Trace-Id' ] || response . header ? .[ 'x-trace-id' ] || '' , fileId : body ? . data ? . fileId || null })
2026-08-10 10:53:11 +08:00
if ( code !== '200' ) { const error = new Error ( body ? . message || '文件上传失败' ); error . code = body ? . code || response . statusCode ; reject ( error ); return }
resolve ( body ? . data || body )
},
2026-08-15 11:36:56 +08:00
fail ( error ) { debugLog ( 'evidence.upload.error' , { inboundId , evidenceType , message : error . errMsg || '' }); const next = new Error ( error . errMsg || '网络连接失败' ); next . network = true ; reject ( next ) }
2026-08-10 10:53:11 +08:00
})
if ( task && typeof task . onProgressUpdate === 'function' && typeof onProgress === 'function' ) task . onProgressUpdate (({ progress }) => onProgress ( progress ))
})
}
2026-08-06 13:56:01 +08:00
export function completeLoading ( inboundId , payload , tenantId ) {
return request ({
url : `/inbounds/ ${ encodeURIComponent ( inboundId ) } /loading/complete?tenantId= ${ encodeURIComponent ( tenantQuery ( tenantId )) } ` ,
method : 'POST' ,
header : { 'Idempotency-Key' : idempotencyKey ( 'complete-loading' , inboundId ) },
data : payload
})
}
2026-08-17 09:41:16 +08:00
/** Complete the cabinet PDA task after the operator has captured all evidence. */
export function completeContainerPickingTask ( taskId , payload = {}) {
if ( ! taskId ) throw new Error ( '缺少拣货任务标识,无法确认装柜完成' )
if ( payload . expectedTaskVersion == null ) throw new Error ( '缺少拣货任务版本,无法确认装柜完成' )
if ( ! payload . operationId ) throw new Error ( '缺少操作标识,无法确认装柜完成' )
return request ({
url : `/first-leg/pda-loading/tasks/ ${ encodeURIComponent ( taskId ) } /complete` ,
method : 'POST' ,
header : { 'Idempotency-Key' : stableIdempotencyKey ( payload . operationId ) },
data : payload
})
}
2026-08-10 10:53:11 +08:00
export { EVIDENCE_SLOTS , EVIDENCE_TYPES }