2026-08-06 13:56:01 +08:00
import { session } from '@/stores/session.js'
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.
function request ( options ) {
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 )
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 = '' ) {
return `pda- ${ operationId }${ suffix ? `- ${ suffix } ` : '' } `
}
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 ) {
return Boolean ( session . token && session . tenantId && inboundId )
}
2026-08-10 10:53:11 +08:00
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 ()) } ` )
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
return data
})
}
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 } ` }
})
}
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 )
}
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 || []) {
2026-08-10 11:53:43 +08:00
const common = { warehouseId , locationIds : action . locationIds || [], version : action . version ?? version , operationId : ` ${ submitOperationId } - ${ action . itemId } ` }
2026-08-10 10:53:11 +08:00
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
}
2026-08-06 13:56:01 +08:00
export function listLoadingOrders ({ page = 1 , size = 50 , keyword = '' } = {}) {
const query = [ `mainStatus=LOADING` , `page= ${ page } ` , `size= ${ size } ` ]
if ( session . tenantId ) query . push ( `tenantId= ${ encodeURIComponent ( session . tenantId ) } ` )
if ( keyword ) query . push ( `keyword= ${ encodeURIComponent ( keyword ) } ` )
return request ({ url : `/orders? ${ query . join ( '&' ) } ` , method : 'GET' }). then (( data ) => ({
... data ,
records : ( data && data . records ? data . records : []). map ( normalizePdaOrder )
}))
}
export function normalizePdaOrder ( row = {}) {
row = row || {}
return {
... row ,
inboundId : row . inboundId ?? row . inbound ? . id ?? null ,
inboundVersion : row . inboundVersion ?? row . inbound ? . version ?? null ,
pickingVersion : row . pickingVersion ?? row . loading ? . pickingVersion ?? null
}
}
export function normalizePickingTask ( task = {}) {
task = task || {}
return {
... task ,
pickingVersion : task . pickingVersion ?? task . version ?? null ,
inboundVersion : task . inboundVersion ?? null
}
}
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 ) {
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 }