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-25 20:50:01 +08:00
import { apiDownload , apiRequest , apiUpload } from '@/api/http.js'
2026-08-27 14:30:26 +08:00
import { resolveEvidencePreviewUrl } from '@/utils/api-url.js'
export { resolveEvidencePreviewUrl } from '@/utils/api-url.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-31 18:08:43 +08:00
// Picking and cabinet evidence are both scoped by the canonical PDA task. The
// task warehouse is always sent as an authorization boundary and never guessed.
2026-08-06 13:56:01 +08:00
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-25 20:50:01 +08:00
const request = apiRequest
2026-08-06 13:56:01 +08:00
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
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 ,
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-30 17:36:57 +08:00
warehouseName : row . warehouseName ?? row . warehouse ? . name ?? row . loadingPlan ? . warehouseName ?? '' ,
orderNo : row . orderNo ?? row . order ? . orderNo ?? '' ,
inboundCode : row . inboundCode ?? row . inbound ? . inboundCode ?? '' ,
displayNo : row . orderNo ?? row . order ? . orderNo ?? row . inboundCode ?? row . inbound ? . inboundCode ?? ''
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 ,
2026-08-30 17:36:57 +08:00
orderNo : task . orderNo ?? task . order ? . orderNo ?? '' ,
inboundCode : task . inboundCode ?? task . inbound ? . inboundCode ?? '' ,
warehouseName : task . warehouseName ?? task . warehouse ? . name ?? '' ,
2026-08-12 18:32:31 +08:00
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-31 18:08:43 +08:00
export function getEvidence ( taskId , warehouseId ) {
if ( ! taskId ) throw new Error ( '缺少装柜任务标识,无法加载装柜证据' )
if ( ! warehouseId ) throw new Error ( '缺少任务仓库标识,无法加载装柜证据' )
2026-08-06 13:56:01 +08:00
return request ({
2026-08-31 18:08:43 +08:00
url : `/first-leg/pda-loading/tasks/ ${ encodeURIComponent ( taskId ) } /evidence?warehouseId= ${ encodeURIComponent ( warehouseId ) } ` ,
2026-08-06 13:56:01 +08:00
method : 'GET'
})
}
2026-08-31 18:08:43 +08:00
export function getEvidencePreviewUrl ( taskId , evidenceId , warehouseId ) {
if ( ! taskId || ! evidenceId || ! warehouseId ) return ''
return `/api/platform/v1/first-leg/pda-loading/tasks/ ${ encodeURIComponent ( taskId ) } /evidence/ ${ encodeURIComponent ( evidenceId ) } /content?warehouseId= ${ encodeURIComponent ( warehouseId ) } `
2026-08-15 12:33:33 +08:00
}
export function downloadEvidencePreview ( previewUrl ) {
requireOnline ( '当前网络不可用,无法查看装柜照片' )
if ( ! previewUrl ) throw new Error ( '装柜照片地址尚未同步' )
2026-08-27 14:30:26 +08:00
const url = resolveEvidencePreviewUrl ( previewUrl , session . apiBase || '/api/platform/v1' )
2026-08-15 12:33:33 +08:00
debugLog ( 'evidence.preview.start' , { previewPath : String ( previewUrl ), resolvedUrl : url })
2026-08-25 20:50:01 +08:00
return apiDownload ({ url }). then (( response ) => {
2026-08-15 12:33:33 +08:00
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
2026-08-25 20:50:01 +08:00
throw error
2026-08-15 12:33:33 +08:00
}
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
2026-08-25 20:50:01 +08:00
throw error
2026-08-15 12:33:33 +08:00
}
2026-08-25 20:50:01 +08:00
return response . tempFilePath
}). catch (( error ) => {
debugLog ( 'evidence.preview.error' , { previewPath : String ( previewUrl ), message : error . message || error . errMsg || '' })
throw error
2026-08-15 12:33:33 +08:00
})
}
2026-08-31 18:08:43 +08:00
export function voidEvidence ( taskId , evidenceId , warehouseId , payload = {}) {
if ( ! taskId || ! evidenceId ) throw new Error ( '缺少装柜任务或证据标识,无法作废' )
if ( ! warehouseId ) throw new Error ( '缺少任务仓库标识,无法作废装柜证据' )
2026-08-10 11:53:43 +08:00
if ( ! String ( payload . reason || '' ). trim ()) throw new Error ( '作废原因不能为空' )
return request ({
2026-08-31 18:08:43 +08:00
url : `/first-leg/pda-loading/tasks/ ${ encodeURIComponent ( taskId ) } /evidence/ ${ encodeURIComponent ( evidenceId ) } /void` ,
2026-08-10 11:53:43 +08:00
method : 'POST' ,
2026-08-31 18:08:43 +08:00
data : { warehouseId , expectedVersion : payload . expectedVersion ?? 0 , reason : String ( payload . reason ). trim () }
2026-08-06 13:56:01 +08:00
})
}
2026-08-10 10:53:11 +08:00
/** Upload a local photo to the file service and return its durable fileId. */
2026-08-31 18:08:43 +08:00
export function uploadEvidenceFile ( taskId , warehouseId , payload , localPath , onProgress ) {
2026-08-11 11:31:05 +08:00
requireOnline ( '当前网络不可用,无法上传装柜照片' )
2026-08-31 18:08:43 +08:00
if ( ! taskId ) throw new Error ( '缺少装柜任务标识,无法上传装柜照片' )
if ( ! warehouseId ) throw new Error ( '缺少任务仓库标识,无法上传装柜照片' )
const evidenceType = payload ? . evidenceType
2026-08-10 10:53:11 +08:00
if ( ! EVIDENCE_TYPES . includes ( evidenceType )) throw new Error ( '不支持的装柜证据类型' )
if ( ! localPath ) throw new Error ( '缺少待上传的照片' )
2026-08-31 18:08:43 +08:00
if ( payload . expectedVersion == null ) throw new Error ( '缺少装柜证据版本,无法上传装柜照片' )
if ( evidenceType === 'SEAL' && ! String ( payload . sealNo || '' ). trim ()) throw new Error ( '带锁码照必须填写封签号' )
2026-08-10 10:53:11 +08:00
const baseUrl = session . apiBase || '/api/platform/v1'
2026-08-31 18:08:43 +08:00
debugLog ( 'evidence.upload.start' , { taskId , evidenceType , hasLocalPath : Boolean ( localPath ) })
const query = [
`warehouseId= ${ encodeURIComponent ( warehouseId ) } ` ,
`expectedVersion= ${ encodeURIComponent ( payload . expectedVersion ) } ` ,
`evidenceType= ${ encodeURIComponent ( evidenceType ) } `
]
if ( payload . sealNo ) query . push ( `sealNo= ${ encodeURIComponent ( String ( payload . sealNo ). trim ()) } ` )
if ( payload . remark ) query . push ( `remark= ${ encodeURIComponent ( payload . remark ) } ` )
2026-08-25 20:50:01 +08:00
return apiUpload ({
2026-08-31 18:08:43 +08:00
url : ` ${ baseUrl } /first-leg/pda-loading/tasks/ ${ encodeURIComponent ( taskId ) } /evidence? ${ query . join ( '&' ) } ` ,
2026-08-10 10:53:11 +08:00
filePath : localPath ,
name : 'file' ,
2026-08-31 18:08:43 +08:00
header : { 'Idempotency-Key' : stableIdempotencyKey ( payload . operationId || `evidence- ${ taskId } - ${ evidenceType } - ${ payload . expectedVersion } ` ) }
2026-08-25 20:50:01 +08:00
}, onProgress ). then (( response ) => {
2026-08-10 10:53:11 +08:00
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-31 18:08:43 +08:00
debugLog ( 'evidence.upload.response' , { taskId , 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 })
if ( code !== '200' ) {
const error = new Error ( body ? . message || '文件上传失败' )
error . code = body ? . code || response . statusCode
error . statusCode = response . statusCode
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' ] || ''
throw error
}
2026-08-25 20:50:01 +08:00
return body ? . data || body
2026-08-31 18:08:43 +08:00
}). catch (( error ) => { debugLog ( 'evidence.upload.error' , { taskId , evidenceType , message : error . message || error . errMsg || '' }); throw error })
2026-08-06 13:56:01 +08:00
}
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 }