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 ) => {
2026-08-15 09:47:28 +08:00
debugLog ( 'api.request' , { method : options . method || 'GET' , url : options . url })
2026-08-06 13:56:01 +08:00
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 09:47:28 +08:00
debugLog ( 'api.response' , {
method : options . method || 'GET' , url : options . url , 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' ] || '' ,
dataKeys : body . data && typeof body . data === 'object' ? Object . keys ( body . data ) : []
})
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 ) {
2026-08-15 09:47:28 +08:00
debugLog ( 'api.network-error' , { method : options . method || 'GET' , url : options . url , errMsg : error . errMsg || '' })
2026-08-06 13:56:01 +08:00
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
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 ) } ` )
2026-08-11 11:31:05 +08:00
if ( session . tenantId ) query . push ( `tenantId= ${ encodeURIComponent ( session . tenantId ) } ` )
2026-08-10 10:53:11 +08:00
return request ({ url : `/first-leg/pda-loading/tasks? ${ query . join ( '&' ) } ` , method : 'GET' }). then (( data ) => {
2026-08-12 18:32:31 +08:00
const task = Array . isArray ( data ) ? data [ 0 ] : ( data ? . records ? data . records [ 0 ] : data )
return task ? normalizePickingTask ( task ) : null
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 ,
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-11 20:39:38 +08:00
displayNo : row . orderNo ?? row . inboundCode ?? row . order ? . orderNo ?? `任务 ${ row . taskId ?? row . id } `
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 ,
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'
})
}
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
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 )
if ( code !== '200' ) { const error = new Error ( body ? . message || '文件上传失败' ); error . code = body ? . code || response . statusCode ; reject ( error ); return }
resolve ( body ? . data || body )
},
fail ( error ) { const next = new Error ( error . errMsg || '网络连接失败' ); next . network = true ; reject ( next ) }
})
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-10 10:53:11 +08:00
export { EVIDENCE_SLOTS , EVIDENCE_TYPES }