From 59e81516a486f4ba6608b2bd92a64c723389cbf7 Mon Sep 17 00:00:00 2001
From: ouyangxq <775153912@qq.com>
Date: Mon, 26 Oct 2026 15:51:10 +0800
Subject: [PATCH 1/3] fix(pda): simplify dispatch review and loading
confirmation
---
src/components/DispatchLoadingDocuments.vue | 6 +--
src/pages/dispatch/dispatch.vue | 43 +++++++++++++--------
tests/warehouse-transfer-api.test.mjs | 5 ++-
tests/warehouse-transfer-counting.test.mjs | 7 ++--
4 files changed, 35 insertions(+), 26 deletions(-)
diff --git a/src/components/DispatchLoadingDocuments.vue b/src/components/DispatchLoadingDocuments.vue
index 3db4448..d42babd 100644
--- a/src/components/DispatchLoadingDocuments.vue
+++ b/src/components/DispatchLoadingDocuments.vue
@@ -1,19 +1,17 @@
- 装车照片每类 1 张,上传后自动保存,可更换
+ 装车照片
{{ busy ? '同步中' : snapshot ? `${snapshot.photos.length}/5 已保存` : '待同步' }}
- 可边装车边上传;确认装车完成且照片齐全后自动进入派送中。
- 上传并保存 5 张装车照片后,本车自动进入派送中。
{{ error.message }}requestId:{{ error.requestId }}traceId:{{ error.traceId }}
diff --git a/src/pages/dispatch/dispatch.vue b/src/pages/dispatch/dispatch.vue
index 708c407..a15651c 100644
--- a/src/pages/dispatch/dispatch.vue
+++ b/src/pages/dispatch/dispatch.vue
@@ -36,20 +36,25 @@
理货员审核
- 车牌号:{{ current.resources?.vehicle?.plate || '—' }} · 托车公司:{{ current.resources?.vehicle?.carrierName || '—' }}
- 司机:{{ current.resources?.driver?.name || '—' }} · 电话:{{ current.resources?.driver?.phone || '—' }}
- 计划发车(北京时间):{{ reviewTime(current.plannedDepartureAt) }}
- 预计送达(北京时间):{{ reviewTime(current.estimatedArrivalAt) }}
- {{ appointment.destinationWarehouseCode }} · {{ appointment.appointmentNo || '—' }} · {{ appointment.status === 'SUCCESS' ? '已确认' : appointment.status === 'REGISTERED' ? '已登记' : '未填写' }}:{{ reviewTime(appointment.confirmedAt) }}(北京时间)
- 驾驶证号:{{ current.resources?.driver?.evidence?.licenseNo || '—' }} · 有效期:{{ current.resources?.driver?.evidence?.licenseExpiresOn || '—' }}
- 调度审核:{{ reviewLabel(current.reviews?.dispatchReview) }} · {{ current.reviews?.dispatchReview?.reviewedAt || '—' }}
- 调度意见:{{ current.reviews.dispatchReview.reason }}
+
+ 车牌号{{ current.resources?.vehicle?.plate || '—' }}
+ 托车公司{{ current.resources?.vehicle?.carrierName || '—' }}
+ 司机{{ current.resources?.driver?.name || '—' }}
+ 电话{{ current.resources?.driver?.phone || '—' }}
+ 计划发车{{ reviewTime(current.plannedDepartureAt) }}(北京时间)
+ 预计送达{{ reviewTime(current.estimatedArrivalAt) }}(北京时间)
+ {{ appointment.destinationWarehouseCode }}{{ appointment.appointmentNo || '—' }}{{ appointment.status === 'SUCCESS' ? '已确认' : appointment.status === 'REGISTERED' ? '已登记' : '未填写' }}:{{ reviewTime(appointment.confirmedAt) }}(北京时间)
+ 驾驶证号{{ current.resources?.driver?.evidence?.licenseNo || '—' }}
+ 有效期{{ current.resources?.driver?.evidence?.licenseExpiresOn || '—' }}
+ 调度审核{{ reviewLabel(current.reviews?.dispatchReview) }} · {{ reviewTime(current.reviews?.dispatchReview?.reviewedAt) }}
+ 调度意见{{ current.reviews.dispatchReview.reason }}
+
已退回客服处理(车辆信息):{{ rejectionReason }}。修改后须重新提交审核。
{{ current.actions.LOADING_REVIEW.blockReason }}
请核对车辆、司机及下方货物;通过后才能开始真实拣货。驳回将回到客服处理(车辆信息)。
-
+
{{ current.actions.LOADING_REVIEW?.blockReason || '当前不具备理货审核资格,请先完成客服提交和调度审核' }}
@@ -72,16 +77,15 @@
拣货箱数本次最多可拣 {{ selectedRemaining }} 箱
- 确认装车完成{{ current.pickingReconciled ? 'PDA拣货已核平,可边装车边上传5张装车照片;确认装车完成且照片齐全后自动进入派送中。' : '先完成全部真实拣货,不能补录数量代替核平。' }}{{ current.actions.LOADING_COMPLETE?.blockReason || '尚未获得装车完成资格' }}
+ {{ current.actions.LOADING_COMPLETE?.blockReason || '尚未获得装车完成资格' }}
{{ successMessage }}
- 确认装车完成{{ loadingPreview.dispatchNo }}
+ 确认装车
{{ loadingRows.length }} 托已拣托盘{{ loadingRows.reduce((sum, row) => sum + Number(row.pickedPackages || 0), 0) }} 箱已拣箱数
- 本车货物已全部装车?照片可在“装车拍照”中继续上传
-
+
@@ -244,20 +248,25 @@ export default {
.stage-tab::after { border: 0; }
.stage-tab.active { color: #1262f4; background: #fff; box-shadow: 0 1px 4px rgba(31, 41, 55, .12); }
.stage-tab[disabled] { opacity: .55; }
-.review-card textarea { box-sizing: border-box; width: 100%; min-height: 160rpx; padding: 16rpx; border: 1px solid #cbd5e1; border-radius: 8rpx; }
+.review-facts { margin: 16rpx 0; }
+.review-fact { display: flex; align-items: flex-start; gap: 20rpx; padding: 10rpx 0; color: #243c58; font-size: 26rpx; line-height: 1.5; }
+.review-label { flex: 0 0 132rpx; color: #718095; overflow-wrap: anywhere; }
+.review-fact > :last-child { flex: 1; min-width: 0; overflow-wrap: anywhere; }
+.review-appointment { display: flex; flex-direction: column; gap: 4rpx; }
+.review-actions { display: flex; gap: 20rpx; margin-top: 24rpx; }
+.review-actions button { flex: 1; min-width: 0; margin: 0; padding: 0 16rpx; min-height: 88rpx; line-height: 88rpx; font-size: 28rpx; }
+.review-actions .primary-button { min-height: 88rpx; line-height: 88rpx; font-size: 28rpx; }
+.review-card textarea { box-sizing: border-box; width: 100%; height: 160rpx; min-height: 160rpx; margin-top: 20rpx; padding: 16rpx; border: 1px solid #cbd5e1; border-radius: 8rpx; }
.loading-overlay { position: fixed; inset: 0; background: rgba(15, 23, 42, .46); display: flex; align-items: center; justify-content: center; z-index: 100; padding: 40rpx; box-sizing: border-box; }
.loading-dialog { width: 100%; max-width: 620rpx; max-height: 88vh; overflow-y: auto; box-sizing: border-box; background: #fff; border-radius: 28rpx; padding: 36rpx 32rpx 32rpx; box-shadow: 0 24rpx 80rpx rgba(15, 23, 42, .18); }
.loading-dialog-heading { display: flex; flex-direction: column; gap: 10rpx; }
.loading-dialog-title { color: #142c49; font-size: 36rpx; font-weight: 700; line-height: 1.4; }
-.loading-dialog-number { color: #7a8aa0; font-size: 26rpx; line-height: 1.5; overflow-wrap: anywhere; }
.loading-counts { display: flex; margin: 28rpx 0; padding: 24rpx 0; border-radius: 20rpx; background: #f3f7ff; }
.loading-count { flex: 1; display: flex; align-items: center; flex-direction: column; gap: 8rpx; }
.loading-count + .loading-count { border-left: 1px solid #dce6f6; }
.loading-count-value { color: #1764ec; font-size: 48rpx; font-weight: 700; line-height: 1.2; }
.loading-count-unit { font-size: 26rpx; font-weight: 500; }
.loading-count-label { color: #72839b; font-size: 24rpx; }
-.loading-confirm-question { display: block; color: #253a54; font-size: 30rpx; font-weight: 600; line-height: 1.5; }
-.loading-confirm-note { display: block; color: #7a8aa0; font-size: 25rpx; line-height: 1.5; margin-top: 10rpx; }
.loading-dialog-actions { display: flex; gap: 20rpx; margin-top: 32rpx; }
.loading-dialog-actions button { flex: 1; min-width: 0; margin: 0; height: 88rpx; line-height: 88rpx; padding: 0 12rpx; border-radius: 16rpx; font-size: 30rpx; font-weight: 600; }
.loading-dialog-actions button::after { border: 0; }
diff --git a/tests/warehouse-transfer-api.test.mjs b/tests/warehouse-transfer-api.test.mjs
index a4bf222..4a89d9d 100644
--- a/tests/warehouse-transfer-api.test.mjs
+++ b/tests/warehouse-transfer-api.test.mjs
@@ -62,9 +62,10 @@ test('warehouse transfer shows forecast and actual variance and requires counted
assert.doesNotMatch(page, /本托 SKU 箱数|itemCounts/)
})
-test('warehouse transfer retries reuse the pending idempotency key', async () => {
+test('warehouse transfer creates a fresh completion key for each explicit attempt', async () => {
const page = await readFile(new URL('../src/pages/warehouse-transfer/warehouse-transfer.vue', import.meta.url), 'utf8')
- assert.match(page, /if \(!this\.pendingKey\) this\.pendingKey = `pda-transfer-/)
+ assert.doesNotMatch(page, /if \(!this\.pendingKey\) this\.pendingKey = `pda-transfer-/)
+ assert.match(page, /this\.pendingKey = `pda-transfer-/)
assert.match(page, /idempotencyKey: this\.pendingKey/)
assert.equal((page.match(/pendingKey = `pda-transfer-/g) || []).length, 1)
})
diff --git a/tests/warehouse-transfer-counting.test.mjs b/tests/warehouse-transfer-counting.test.mjs
index 8046f3d..4bc4982 100644
--- a/tests/warehouse-transfer-counting.test.mjs
+++ b/tests/warehouse-transfer-counting.test.mjs
@@ -107,12 +107,13 @@ test('transfer counts require valid pieces and measurements and record variance
assert.equal(value.cartonCountReady,true);value.countDiscrepancyReason='多一箱';assert.equal(value.cartonCountReady,true)
assert.equal(value.cartonQuantity,9);assert.equal(value.cartonVolume,'0.012')
})
-test('network retry preserves request key and changed content uses a new request',async()=>{
+test('failed count unlocks the draft and each explicit retry sends current contents with a fresh key',async()=>{
const {value,scope}=setup();const requests=[]
scope.countWarehouseTransferCartons=async(no,request)=>{requests.push(request);throw new Error('offline')}
await value.confirmCartons();await value.confirmCartons()
- assert.equal(requests.length,2);assert.equal(requests[0].idempotencyKey,requests[1].idempotencyKey)
+ assert.equal(requests.length,2);assert.notEqual(requests[0].idempotencyKey,requests[1].idempotencyKey)
value.cartonBoxes[0].items[0].quantity=8;await value.confirmCartons();assert.notEqual(requests[1].idempotencyKey,requests[2].idempotencyKey)
+ assert.equal(requests[2].boxes[0].items[0].quantity,8);assert.equal(value.cartonBoxes[0].items[0].quantity,8)
assert.equal(value.countingCartons,false);assert.equal(value.error.message,'offline')
})
test('draft restores additional cartons and discrepancy reason without crossing tasks',()=>{
@@ -221,7 +222,7 @@ for (const answer of [{confirm:false}, {confirm:true,content:' '}, {confirm:tru
await value.submit()
assert.equal(prompts,1);assert.equal(value.submitting,false)
assert.equal(requests.length,answer.confirm && answer.content?.trim()?1:0)
- if(requests.length){assert.equal(requests[0].discrepancyReason,'少到一箱');await value.submit();assert.equal(requests[0].idempotencyKey,requests[1].idempotencyKey)}
+ if(requests.length){assert.equal(requests[0].discrepancyReason,'少到一箱');await value.submit();assert.notEqual(requests[0].idempotencyKey,requests[1].idempotencyKey)}
})
}
test('matching SKU counts with different carton count submit without prompting, and missing photos block before prompting',async()=>{
From 7a47d3d35b561ecc87ce0e51f2b8964f5c85ce40 Mon Sep 17 00:00:00 2001
From: ouyangxq <775153912@qq.com>
Date: Mon, 26 Oct 2026 16:05:05 +0800
Subject: [PATCH 2/3] fix(pda): shorten home operation labels
---
src/data/overseas-workbench.js | 10 +++++-----
tests/project-contract.test.mjs | 2 +-
tests/workbench-availability.test.mjs | 20 ++++++++++----------
3 files changed, 16 insertions(+), 16 deletions(-)
diff --git a/src/data/overseas-workbench.js b/src/data/overseas-workbench.js
index 66e5483..7def44e 100644
--- a/src/data/overseas-workbench.js
+++ b/src/data/overseas-workbench.js
@@ -2,14 +2,14 @@ export const MODULES = [
{ key: 'inbound', name: '入仓作业', nameKey: 'modules.inbound.name', icon: '入', description: '处理拆柜收货与客户退货实收入仓', descriptionKey: 'modules.inbound.description', operations: [
{ key: 'receiving', name: '拆柜收货', nameKey: 'modules.receiving.name', icon: '卸', iconType: 'download', description: '三类照片签收,箱码定位入仓码后按目的类型收货', descriptionKey: 'modules.receiving.description', available: true },
{ key: 'warehouseTransferList', name: '仓储转运测量列表', nameKey: 'modules.warehouseTransferList.name', icon: '列', iconType: 'list', description: '查看待测量的仓储转运单', descriptionKey: 'modules.warehouseTransferList.description', available: true, route: '/pages/warehouse-transfer/warehouse-transfer?mode=list' },
- { key: 'warehouseTransferWork', name: '转运测量作业', nameKey: 'modules.warehouseTransferWork.name', icon: '转', iconType: 'compose', description: '进入转运单详情完成测量、打托和拍照', descriptionKey: 'modules.warehouseTransferWork.description', available: true, route: '/pages/warehouse-transfer/warehouse-transfer?mode=work' },
+ { key: 'warehouseTransferWork', name: '转运测量', nameKey: 'modules.warehouseTransferWork.name', icon: '转', iconType: 'compose', description: '进入转运单详情完成测量、打托和拍照', descriptionKey: 'modules.warehouseTransferWork.description', available: true, route: '/pages/warehouse-transfer/warehouse-transfer?mode=work' },
{ key: 'returnReceipt', name: '退货入库', nameKey: 'modules.returnReceipt.name', icon: '退', iconType: 'undo', description: '扫描退货单或 SKU,按本包实收数量生成退货库存', descriptionKey: 'modules.returnReceipt.description', available: true, route: '/pages/return-receipt/return-receipt' },
- { key: 'returnPacking', name: '退货合箱', nameKey: 'modules.returnPacking.name', icon: '合', iconType: 'folder-add', description: '选择同客户已签收退货单,按 SKU 数量生成新箱', descriptionKey: 'modules.returnPacking.description', available: true, route: '/pages/return-packing/return-packing' },
- { key: 'returnCounting', name: '退货箱内清点', icon: '清', iconType: 'list', description: '逐箱清点托盘签收货物,核对 SKU 和数量', available: true, route: '/pages/return-counting/return-counting' }
+ { key: 'returnPacking', name: '合箱', nameKey: 'modules.returnPacking.name', icon: '合', iconType: 'folder-add', description: '选择同客户已签收退货单,按 SKU 数量生成新箱', descriptionKey: 'modules.returnPacking.description', available: true, route: '/pages/return-packing/return-packing' },
+ { key: 'returnCounting', name: '退货清点', icon: '清', iconType: 'list', description: '逐箱清点托盘签收货物,核对 SKU 和数量', available: true, route: '/pages/return-counting/return-counting' }
] },
{ key: 'outbound', name: '出库履约', nameKey: 'modules.outbound.name', icon: '出', description: '排车拣货、出库任务与发车闭环', descriptionKey: 'modules.outbound.description', operations: [
- { key: 'dispatchPicking', name: '排车拣货', nameKey: 'modules.dispatchPicking.name', icon: '车', iconType: 'paperplane', description: '理货审核通过后真实拣货,确认装车完成即进入派送中', descriptionKey: 'modules.dispatchPicking.description', available: true, route: '/pages/dispatch/dispatch' },
- { key: 'outboundPicking', name: '出库作业', nameKey: 'modules.outboundPicking.name', icon: '拣', iconType: 'checkbox', description: '处理普通出库、一件代发和废弃订单,按托盘扫码完成', descriptionKey: 'modules.outboundPicking.description', available: true, route: '/pages/outbound/outbound' }
+ { key: 'dispatchPicking', name: '拣货', nameKey: 'modules.dispatchPicking.name', icon: '车', iconType: 'paperplane', description: '理货审核通过后真实拣货,确认装车完成即进入派送中', descriptionKey: 'modules.dispatchPicking.description', available: true, route: '/pages/dispatch/dispatch' },
+ { key: 'outboundPicking', name: '出库', nameKey: 'modules.outboundPicking.name', icon: '拣', iconType: 'checkbox', description: '处理普通出库、一件代发和废弃订单,按托盘扫码完成', descriptionKey: 'modules.outboundPicking.description', available: true, route: '/pages/outbound/outbound' }
] },
{ key: 'inventory', name: '库内作业', nameKey: 'modules.inventory.name', icon: '库', description: '商品移库与托盘管理', descriptionKey: 'modules.inventory.description', operations: [
{ key: 'move', name: '商品移库', nameKey: 'modules.move.name', icon: '移', iconType: 'loop', description: '扫描来源与目标库位完成移库', descriptionKey: 'modules.move.description', available: true, route: '/pages/move/move' },
diff --git a/tests/project-contract.test.mjs b/tests/project-contract.test.mjs
index 58f99be..5246fc3 100644
--- a/tests/project-contract.test.mjs
+++ b/tests/project-contract.test.mjs
@@ -31,7 +31,7 @@ test('overseas workbench contains confirmed warehouse and phase-five outbound do
const [data, session, runtime] = await Promise.all([
read('../src/data/overseas-workbench.js'), read('../src/stores/session.js'), read('../src/config/runtime.js')
])
- for (const label of ['入仓作业', '拆柜收货', '退货入库', '退货合箱', '出库履约', '排车拣货', '出库作业', '库内作业', '换标作业', '现场查询']) assert.match(data, new RegExp(label))
+ for (const label of ['入仓作业', '拆柜收货', '退货入库', '合箱', '出库履约', '拣货', '出库', '库内作业', '换标作业', '现场查询']) assert.match(data, new RegExp(label))
for (const excluded of ['集装箱拣货', '装柜取证', '海外派送', '派送单', '客户回访']) assert.doesNotMatch(data, new RegExp(excluded))
assert.match(session, /overseas-pda-auth-session/)
assert.match(runtime, /VITE_API_BASE_URL/)
diff --git a/tests/workbench-availability.test.mjs b/tests/workbench-availability.test.mjs
index 372fb88..847406d 100644
--- a/tests/workbench-availability.test.mjs
+++ b/tests/workbench-availability.test.mjs
@@ -7,12 +7,12 @@ test('only operations backed by a dedicated overseas PDA contract are enabled',
const enabled = operations.filter((operation) => operation.available)
assert.deepEqual(enabled.map(({ module, name }) => ({ module, name })), [
{ module: 'inbound', name: '拆柜收货' },
- { module: 'inbound', name: '仓储转运测量列表' }, { module: 'inbound', name: '转运测量作业' },
+ { module: 'inbound', name: '仓储转运测量列表' }, { module: 'inbound', name: '转运测量' },
{ module: 'inbound', name: '退货入库' },
- { module: 'inbound', name: '退货合箱' },
- { module: 'inbound', name: '退货箱内清点' },
- { module: 'outbound', name: '排车拣货' },
- { module: 'outbound', name: '出库作业' },
+ { module: 'inbound', name: '合箱' },
+ { module: 'inbound', name: '退货清点' },
+ { module: 'outbound', name: '拣货' },
+ { module: 'outbound', name: '出库' },
{ module: 'inventory', name: '商品移库' },
{ module: 'inventory', name: '托盘管理' },
{ module: 'relabel', name: '换标列表' },
@@ -23,13 +23,13 @@ test('only operations backed by a dedicated overseas PDA contract are enabled',
{ module: 'query', name: '托盘查询' }
])
assert.equal(operations.find((item) => item.name === '托盘管理')?.route, '/pages/pallet/pallet')
- assert.equal(operations.find((item) => item.name === '排车拣货')?.route, '/pages/dispatch/dispatch')
- assert.equal(operations.find((item) => item.name === '出库作业')?.route, '/pages/outbound/outbound')
+ assert.equal(operations.find((item) => item.name === '拣货')?.route, '/pages/dispatch/dispatch')
+ assert.equal(operations.find((item) => item.name === '出库')?.route, '/pages/outbound/outbound')
assert.equal(operations.find((item) => item.name === '退货入库')?.route, '/pages/return-receipt/return-receipt')
- assert.equal(operations.find((item) => item.name === '退货合箱')?.route, '/pages/return-packing/return-packing')
- assert.equal(operations.find((item) => item.name === '退货箱内清点')?.route, '/pages/return-counting/return-counting')
+ assert.equal(operations.find((item) => item.name === '合箱')?.route, '/pages/return-packing/return-packing')
+ assert.equal(operations.find((item) => item.name === '退货清点')?.route, '/pages/return-counting/return-counting')
assert.equal(operations.find((item) => item.name === '仓储转运测量列表')?.route, '/pages/warehouse-transfer/warehouse-transfer?mode=list')
- assert.equal(operations.find((item) => item.name === '转运测量作业')?.route, '/pages/warehouse-transfer/warehouse-transfer?mode=work')
+ assert.equal(operations.find((item) => item.name === '转运测量')?.route, '/pages/warehouse-transfer/warehouse-transfer?mode=work')
assert.equal(operations.some((item) => item.name === '库存盘点'), false)
From 7749b33700967329b7e626ac7f7e9c7f9a352909 Mon Sep 17 00:00:00 2001
From: ouyangxq <775153912@qq.com>
Date: Mon, 26 Oct 2026 17:12:38 +0800
Subject: [PATCH 3/3] feat(pda): support five loading photos per category and
reconcile saves
---
docs/business-index.md | 4 +-
src/api/dispatch-loading-documents.js | 10 ++-
src/components/DispatchLoadingDocuments.vue | 67 ++++++++++++++------
tests/dispatch-loading-documents.test.mjs | 70 +++++++++++++++++++--
4 files changed, 120 insertions(+), 31 deletions(-)
diff --git a/docs/business-index.md b/docs/business-index.md
index 87f9a8a..21a6cf6 100644
--- a/docs/business-index.md
+++ b/docs/business-index.md
@@ -25,7 +25,7 @@
| 商品移库真实作业 | `src/pages/move/move.vue`、`src/api/warehouse-operations.js`、`src/stores/warehouse-work.js` | 扫描来源托盘与目标库位执行同仓移库;冲突后重新读取服务端事实 |
| 换标真实作业 | `src/pages/relabel/relabel.vue`、`src/api/relabel.js` | 同一页面按操作参数承载换标收货、执行与复核;执行页按当前单明细切换 SKU/箱唛 Tab,组合明细同时显示两类需求。SKU 逐条扫码旧新SKU并携带审批件数,箱唛扫码仓库唛头/新FBA箱唛;组合单按同箱保留两个Tab的核对输入,全部核对后只提交一次箱事实。认领同步处理中;支持拍照、异常/返工和幂等恢复 |
| 现场查询真实作业 | `src/pages/query/query.vue`、`src/api/warehouse-operations.js` | 同一页面按操作参数承载库存、库位与托盘查询,只展示稳定业务标识 |
-| 排车拣货与发车/理货审核 | `src/pages/dispatch/dispatch.vue`、`src/api/fulfillment.js`、`src/stores/fulfillment.js` | 普通进入展示任务列表和“上次作业”,不因本地草稿自动打开详情;明确跳转 `?resume=1` 才恢复排车。默认任务列表含待理货排车,详情展示服务端车辆/司机快照、计划发车及预计送达时间、当前有效FBA收货确认、调度审核状态/意见和货物;时间明确标注北京时间。预约或审核依据失效时 `APPROVAL_BLOCKED` 禁止批准但保留驳回。仅 `actions.LOADING_REVIEW.allowed=true` 时可调用 `POST /api/pda/v1/last-leg/dispatches/{dispatchNo}/loading-review`,提交原 `recordVersion/idempotencyKey/decision/reason`;驳回原因必填,任一级拒绝统一回客服处理(车辆信息),重新提交后再次逐级审核。PDA 不执行调度审核;理货通过并重读资格后才允许按实物托盘码或来源箱唛真实拣货,服务端 `actions.PICK/LOADING_COMPLETE` 分别决定真实拣货和装车完成资格;按已扫托数、箱数整车一次确认装车,不逐托填重量、体积或托位,资料未齐进入 `AFTER_LOADING`、已齐直接进入 `DELIVERING`;详情采用“拣货作业/装车拍照”两个 Tab,打开任务默认拣货;切换保留拣货草稿与照片状态,拍照页不响应拣货扫码,同单照片保存刷新保留当前 Tab。装车中即可在拍照 Tab 通过 `src/components/DispatchLoadingDocuments.vue` 上传五张整车照片,照片齐全且确认装车完成后由服务端在最后一项完成的事务自动生成派送并发车;不额外安排派送或确认发车。公网前缀 `/api/pda/v1/last-leg/dispatches/**`。回归 `tests/dispatch-workflow.test.mjs tests/fulfillment-workflows.test.mjs` |
+| 排车拣货与发车/理货审核 | `src/pages/dispatch/dispatch.vue`、`src/api/fulfillment.js`、`src/stores/fulfillment.js` | 普通进入展示任务列表和“上次作业”,不因本地草稿自动打开详情;明确跳转 `?resume=1` 才恢复排车。默认任务列表含待理货排车,详情展示服务端车辆/司机快照、计划发车及预计送达时间、当前有效FBA收货确认、调度审核状态/意见和货物;时间明确标注北京时间。预约或审核依据失效时 `APPROVAL_BLOCKED` 禁止批准但保留驳回。仅 `actions.LOADING_REVIEW.allowed=true` 时可调用 `POST /api/pda/v1/last-leg/dispatches/{dispatchNo}/loading-review`,提交原 `recordVersion/idempotencyKey/decision/reason`;驳回原因必填,任一级拒绝统一回客服处理(车辆信息),重新提交后再次逐级审核。PDA 不执行调度审核;理货通过并重读资格后才允许按实物托盘码或来源箱唛真实拣货,服务端 `actions.PICK/LOADING_COMPLETE` 分别决定真实拣货和装车完成资格;按已扫托数、箱数整车一次确认装车,不逐托填重量、体积或托位,资料未齐进入 `AFTER_LOADING`、已齐直接进入 `DELIVERING`;详情采用“拣货作业/装车拍照”两个 Tab,打开任务默认拣货;切换保留拣货草稿与照片状态,拍照页不响应拣货扫码,同单照片保存刷新保留当前 Tab。装车中即可在拍照 Tab 通过 `src/components/DispatchLoadingDocuments.vue` 上传五类整车照片(司机驾驶证、车牌照片、装车完成、封车照片、BOL司机签字,每类1至5张),照片齐全且确认装车完成后由服务端在最后一项完成的事务自动生成派送并发车;不额外安排派送或确认发车。公网前缀 `/api/pda/v1/last-leg/dispatches/**`。回归 `tests/dispatch-workflow.test.mjs tests/fulfillment-workflows.test.mjs` |
| 装车确认、自动派送与证照提醒 | `src/pages/dispatch/dispatch.vue`、`src/api/fulfillment.js` 的 `completeDispatchLoading` | 一单一趟,使用 `dispatchNo`、公开 `recordVersion`(0 合法)及本次 `idempotencyKey`。只在请求中锁定,结束即释放;保留表单,不持久化或重放失败命令,下次点击按当前输入和版本生成新键。结果不明或 409 先刷新权威任务,不以超时推断未执行或成功。结果核对单号、动作、请求键和版本;理货审核通过须 `stage=LOADING`,驳回须 `stage=BEFORE_LOADING`;`LOADING_COMPLETE` 返回 `stage=AFTER_LOADING|DELIVERING`、Loaded 结果,提交版本、manifestHash 和幂等键,不逐托填测量。成功后重读任务,版本不得早于已确认结果;重读失败只刷新,不自动重复 POST。司机证照、资源及各客户派送资格由服务端校验;正式流程见工作区 `docs/海外仓业务/排车列表统一作业改造契约.md`,不使用独立车次发车请求 |
| 普通/一件代发出库 | `src/pages/outbound/outbound.vue`、`src/api/fulfillment.js`、`src/stores/fulfillment.js` | 首次进入清空扫描框并展示任务列表和“上次作业”标记,不自动恢复旧详情;详情提供独立返回列表入口。逐箱扫码、DP 托盘整托拣取及幂等完成保持不变;扫描排车业务号显式跳转排车 `?resume=1`。公网前缀 `/api/pda/v1/last-leg/outbound-tasks/**`;派送由 Admin/Client 承载 |
| 扫码设备与诊断 | `src/utils/scan-broadcast.js`、`src/pages/scanner-test/scanner-test.vue` | `src/components/ScanTargetInput.vue`、声音反馈工具;广播 Action/Extra 见本文件服务边界 |
@@ -61,5 +61,5 @@
- 库内、换标与现场查询只调用 `/api/pda/**` 业务 API/DTO,并统一受 `lastleg:pda:operate`、Token 冻结仓库范围和服务端资源归属约束;写请求携带本次幂等键,结束即释放操作态,409 后重新读取服务端状态,不得把平台、客户或内部接口直接暴露给 PDA。
- 柜号签收入仓调用 Lastleg `/api/pda/v1/last-leg/receiving-containers/**`;退货签收与合箱分别调用 `/api/pda/v1/last-leg/return-receipts/**`、`/api/pda/v1/last-leg/return-inventory/**`,具体入口见上表。浏览器或设备请求必须经统一 `gateway` 的显式 PDA 路由,不得直连 Lastleg。
- 海外业务 Token、PDA 权限和可信身份最终由 Gateway 与服务端校验;前端不得用工作台 `available`、本地客户 ID 或仓库 ID 代替授权和资源归属校验。
-- 排车和出库入口只调用专用 PDA 接口;客户与绑定海外仓由 Token、Gateway 和服务端解析,前端不提交 `customerId`、`warehouseId`,也不展示内部数据库 ID 作为业务编号。动作资格、版本和失败处理见本文件排车条目,装车中可同步上传五张照片,装车确认与资料齐全两项均完成后服务端自动生成派送并发车,装完但资料未齐时留在装车后;照片API为 `/pda/v1/last-leg/dispatches/{dispatchNo}/loading-documents`,前端不补派送费用。完成成功后不得重复提交;PDA 不直接创建派送单,平台与客户的读取范围遵循现行契约。
+- 排车和出库入口只调用专用 PDA 接口;客户与绑定海外仓由 Token、Gateway 和服务端解析,前端不提交 `customerId`、`warehouseId`,也不展示内部数据库 ID 作为业务编号。动作资格、版本和失败处理见本文件排车条目,装车中可同步上传五类照片,每类1至5张,装车确认与资料齐全两项均完成后服务端自动生成派送并发车,装完但资料未齐时留在装车后;照片API为 `/pda/v1/last-leg/dispatches/{dispatchNo}/loading-documents`,前端不补派送费用。完成成功后不得重复提交;PDA 不直接创建派送单,平台与客户的读取范围遵循现行契约。
- 排车拣货、普通出库、整托出库和废弃确认在实际写库存前,先用原扫码 `idempotencyKey` 调用 `POST /api/pda/v1/last-leg/storage-rent/preparations`。四种模式分别为 `DISPATCH/OUTBOUND/OUTBOUND_PALLET/DISCARD`,货源、客户、计租范围和可用资金由服务端校验。仅明确 `ready=true` 且状态为 `NOT_REQUIRED/READY/CONSUMED` 才继续原扫码命令;待冻结、失败、过期、响应异常或网络未知均不发库存写请求。预校验与紧随其后的库存写请求使用同一个键和同一份货源参数;请求结束释放操作态,保留表单但不自动重放失败命令。再次点击按当前输入和版本生成新键,仍必须重新通过服务端费用预校验;结果不明时先刷新权威任务,不以换键跳过费用确认。定向回归为 `tests/storage-rent-preflight.test.mjs`,离线通过不代表实际冻结、扣款和出库联调已验收。
diff --git a/src/api/dispatch-loading-documents.js b/src/api/dispatch-loading-documents.js
index 07b188d..1894179 100644
--- a/src/api/dispatch-loading-documents.js
+++ b/src/api/dispatch-loading-documents.js
@@ -1,6 +1,9 @@
import { overseasRequest, overseasUpload, overseasDownload } from './overseas.js'
export const loadingPhotoSlots = ['LOADING_SCENE', 'LOADING_LIST', 'PRE_DEPARTURE_CHECK', 'DRIVER_LICENSE', 'RECEIPT']
+// 保留既有五框标识以维持文件归属,界面按当前装车资料分类展示。
+export const loadingPhotoLabels = { LOADING_SCENE: '司机驾驶证', LOADING_LIST: '车牌照片', PRE_DEPARTURE_CHECK: '装车完成', DRIVER_LICENSE: '封车照片', RECEIPT: 'BOL司机签字' }
+export const loadingPhotoLimit = 5
const path = no => {
if (!no || typeof no !== 'string') throw new Error('请先打开排车任务')
return `/pda/v1/last-leg/dispatches/${encodeURIComponent(no)}/loading-documents`
@@ -12,10 +15,11 @@ function slotPath(no, slot) {
export function normalizeLoadingDocuments(value, no) {
if (value?.dispatchNo !== no || !/^\d+$/.test(String(value.recordVersion ?? '')) || !Array.isArray(value.photos)
|| !['LOADING', 'AFTER_LOADING', 'DELIVERING', 'DELIVERED'].includes(value.stage)) throw new Error('装车资料与当前排车事实不一致')
- const slots = new Set()
+ const files = new Set(), counts = {}
for (const photo of value.photos) {
- if (!loadingPhotoSlots.includes(photo.slot) || !photo.fileId || slots.has(photo.slot)) throw new Error('装车照片引用不完整或重复')
- slots.add(photo.slot)
+ if (!loadingPhotoSlots.includes(photo.slot) || !photo.fileId || files.has(String(photo.fileId))) throw new Error('装车照片引用不完整或重复')
+ files.add(String(photo.fileId)); counts[photo.slot] = (counts[photo.slot] || 0) + 1
+ if (counts[photo.slot] > loadingPhotoLimit) throw new Error('每类装车照片最多5张')
}
return { ...value, editable: value.editable === true, complete: value.complete === true }
}
diff --git a/src/components/DispatchLoadingDocuments.vue b/src/components/DispatchLoadingDocuments.vue
index d42babd..464b612 100644
--- a/src/components/DispatchLoadingDocuments.vue
+++ b/src/components/DispatchLoadingDocuments.vue
@@ -2,24 +2,27 @@
装车照片
- {{ busy ? '同步中' : snapshot ? `${snapshot.photos.length}/5 已保存` : '待同步' }}
+ {{ busy ? '同步中' : snapshot ? `${completedCategories}/5 类已保存` : '待同步' }}
+ 每类至少 1 张、最多 5 张,上传后自动保存,可逐张更换。
+ 保存状态尚未核实,请先刷新资料后再上传。
{{ error.message }}requestId:{{ error.requestId }}traceId:{{ error.traceId }}
查看大图
+
@@ -29,17 +32,18 @@