diff --git a/AGENTS.md b/AGENTS.md
index 16c82e5..398772c 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -25,7 +25,7 @@
- 工作台定义:`src/data/overseas-workbench.js`。
- 登录与会话:`src/api/auth.js`、`src/stores/session.js`。
- 扫码设备层:`src/utils/scan-broadcast.js`;广播 Action=`android.intent.action.SCANRESULT`,Extra=`value`。
-- 当前通用作业页中的入仓作业已接真实海外 API;其他库内、换标与查询模块仍是静态原型,不得将原型提示改成模拟远程成功。
+- 当前通用作业页中的入仓作业已接柜号级真实海外 API:账号锁柜、三类照片签收、FBA 按 SKU 与箱数收货、OVERSEAS 逐箱收货并逐入仓码核平;其他库内、换标与查询模块仍是静态原型,不得将原型提示改成模拟远程成功。
## 页面与能力快速索引
@@ -41,7 +41,8 @@
## 海外业务接口边界
-- `src/api/inbound.js` 已接入入仓任务、推送、托盘分配、清点、拣货、箱码扫描/移除、卸柜照片和完成入库;双 Token 刷新与海外 401 单次重放由 `src/api/overseas.js` 统一处理。
+- `src/api/inbound.js` 已接入待卸柜候选、柜详情/prepare、current 恢复、三类照片签收、入仓码下 SKU/箱唛解析、实物托盘解析、FBA bulk/OVERSEAS carton 收货及整柜完成;响应与 404/409 判定集中在 `src/api/receiving-adapter.js`,双 Token 刷新与海外 401 单次重放由 `src/api/overseas.js` 统一处理。
+- 请求身份和资源范围只取 Token 与 current 会话;前端不得提交 `customerId`、`planNo`、`allocationId`、`warehouseId` 或 `locationId`。409 必须引导账号恢复当前柜,完成前不得切柜。
- 库内、换标与现场查询仍是静态原型;新增真实作业必须建立明确的业务 API/DTO、错误态和契约测试,不得把原型任务改成模拟成功。
- 已有海外入仓 PDA Controller 统一位于 Lastleg `/api/pda/v1/last-leg/inbound-plans/{planNo}/**`;浏览器或设备请求仍必须经 `gateway-overseas` 的显式 PDA 路由,不得直连 Lastleg,也不得改走国内 Gateway。
- 海外业务 Token、PDA 权限和可信身份最终由 Gateway 与服务端校验;前端不得用工作台 `available`、本地客户 ID 或仓库 ID 代替授权和资源归属校验。
diff --git a/README.md b/README.md
index 9a59baf..84c13be 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
独立的 uni-app Vue 3 工程,面向海外仓现场人员。工程不依赖国内 PDA 源码或仓库外共享目录,可独立安装依赖、运行、编译和发布。
-首版工作台按四期边界提供真实入仓作业,以及库内作业、换标作业和现场查询原型。登录使用国内 Auth 统一签发的双 Token;海外入仓请求统一经 `gateway-overseas`,不得请求国内 Firstleg/Warehouse 业务接口。
+首版工作台按四期边界提供柜号级真实入仓作业,以及库内作业、换标作业和现场查询原型。登录使用国内 Auth 统一签发的双 Token;海外入仓请求统一经 `gateway-overseas`,不得请求国内 Firstleg/Warehouse 业务接口。
## 本地运行与检查
@@ -37,6 +37,7 @@ npm run build:app
- 已复用并内置:登录壳、PDA 样式、扫码广播、声音反馈、导航、网络检测、图标及 `uni_modules`。
- 已隔离:国内集装箱拣货、装柜证据、国内 Firstleg API、国内任务状态和国内本地草稿。
-- 入仓页面已接任务、推送、托盘分配、清点、拣货、箱码扫描/移除、卸柜照片和完成入库的真实 API;错误会保留业务码、requestId 与 traceId。
+- 入仓页面已接待卸柜候选、柜详情与账号锁柜、三类角色照片签收、current 恢复、SKU/箱唛与既有物理托盘解析、FBA 按 SKU 和箱数收货、OVERSEAS 逐箱收货、逐入仓码核平及整柜完成的真实 API;409 会引导返回当前柜,错误会保留业务码、requestId 与 traceId。
+- 入仓请求不手填客户、计划、分配、仓库或库位标识;身份与当前柜范围由 Token、Gateway 和服务端会话确定。页面同时支持 PDA 广播扫码与 H5 人工输入。
- 库内、换标和现场查询仍是可运行的静态原型,不会模拟海外服务提交成功。
- 本地契约测试与 H5/App 资源编译不等于真实开发环境 HTTP 或 PDA 真机验收。
diff --git a/src/api/inbound.js b/src/api/inbound.js
index e82bbc4..d0014f4 100644
--- a/src/api/inbound.js
+++ b/src/api/inbound.js
@@ -1,17 +1,18 @@
import { overseasRequest, overseasUpload } from '@/api/overseas.js'
+import { normalizeReceivingCandidate, normalizeReceivingSession } from '@/api/receiving-adapter.js'
const RECEIVING_BASE = '/pda/v1/last-leg/receiving-containers'
const current = (suffix = '') => `${RECEIVING_BASE}/current${suffix}`
-export const listReceivingCandidates = () => overseasRequest('GET', `${RECEIVING_BASE}/candidates`)
-export const getReceivingContainer = (containerNo) => overseasRequest('GET', `${RECEIVING_BASE}/${encodeURIComponent(containerNo)}`)
-export const prepareReceivingContainer = (containerNo, request) => overseasRequest('POST', `${RECEIVING_BASE}/${encodeURIComponent(containerNo)}/prepare`, request)
+export const listReceivingCandidates = async () => (await overseasRequest('GET', `${RECEIVING_BASE}/candidates`)).map(normalizeReceivingCandidate)
+export const getReceivingContainer = async (containerNo) => normalizeReceivingSession(await overseasRequest('GET', `${RECEIVING_BASE}/${encodeURIComponent(containerNo)}`))
+export const prepareReceivingContainer = async (containerNo, request) => normalizeReceivingSession(await overseasRequest('POST', `${RECEIVING_BASE}/${encodeURIComponent(containerNo)}/prepare`, request))
export const uploadCurrentUnloadPhoto = (filePath) => overseasUpload(current('/unload-photos/files'), filePath)
-export const signCurrentReceiving = (request) => overseasRequest('POST', current('/sign'), request)
-export const getCurrentReceiving = () => overseasRequest('GET', current())
+export const signCurrentReceiving = async (request) => normalizeReceivingSession(await overseasRequest('POST', current('/sign'), request))
+export const getCurrentReceiving = async () => normalizeReceivingSession(await overseasRequest('GET', current()))
export const resolveCurrentCarton = (cartonMark) => overseasRequest('POST', current('/cartons/resolve'), { cartonMark })
export const resolveCurrentSku = (skuCode) => overseasRequest('POST', current('/skus/resolve'), { skuCode })
export const resolveCurrentPallet = (palletCode, inboundCode) => overseasRequest('POST', current('/pallets/resolve'), { palletCode, inboundCode })
export const receiveCurrentBulk = (request) => overseasRequest('POST', current('/receipts/bulk'), request)
export const receiveCurrentCarton = (request) => overseasRequest('POST', current('/receipts/carton'), request)
-export const completeCurrentReceiving = (request) => overseasRequest('POST', current('/complete'), request)
+export const completeCurrentReceiving = async (request) => normalizeReceivingSession(await overseasRequest('POST', current('/complete'), request))
diff --git a/src/api/receiving-adapter.js b/src/api/receiving-adapter.js
new file mode 100644
index 0000000..91720bb
--- /dev/null
+++ b/src/api/receiving-adapter.js
@@ -0,0 +1,45 @@
+export const RECEIVING_PHOTO_ROLES = ['LICENSE_PLATE', 'CONTAINER_SEAL', 'DOOR_OPEN']
+
+const number = (value) => Math.max(0, Number(value || 0))
+
+export function normalizeReceivingInbound(value = {}) {
+ return {
+ ...value,
+ plannedPalletCount: number(value.plannedPalletCount),
+ totalPackages: number(value.totalPackages),
+ receivedPackages: number(value.receivedPackages),
+ pendingPackages: number(value.pendingPackages)
+ }
+}
+
+export function normalizeReceivingCandidate(value = {}) {
+ return {
+ ...value,
+ destinationCodes: Array.isArray(value.destinationCodes) ? value.destinationCodes.filter(Boolean) : [],
+ plannedPalletCount: number(value.plannedPalletCount),
+ totalPackages: number(value.totalPackages),
+ receivedPackages: number(value.receivedPackages),
+ currentAccount: value.currentAccount === true
+ }
+}
+
+export function normalizeReceivingSession(value) {
+ if (!value) return null
+ const photos = Array.isArray(value.photos) ? value.photos : []
+ return {
+ ...value,
+ photos: RECEIVING_PHOTO_ROLES.map((role) => photos.find((photo) => photo?.role === role)).filter(Boolean),
+ totalPackages: number(value.totalPackages),
+ receivedPackages: number(value.receivedPackages),
+ pendingPackages: number(value.pendingPackages),
+ inbounds: Array.isArray(value.inbounds) ? value.inbounds.map(normalizeReceivingInbound) : []
+ }
+}
+
+export function isNoCurrentReceiving(error) {
+ return ['404', 'A0404', 'B0404'].includes(String(error?.code || ''))
+}
+
+export function isReceivingConflict(error) {
+ return ['409', 'A0409', 'B0409'].includes(String(error?.code || ''))
+}
diff --git a/src/data/overseas-workbench.js b/src/data/overseas-workbench.js
index 675cc23..d488797 100644
--- a/src/data/overseas-workbench.js
+++ b/src/data/overseas-workbench.js
@@ -1,6 +1,6 @@
export const MODULES = [
- { key: 'inbound', name: '入仓作业', icon: '入', description: '柜号签收、卸柜收货与入库', operations: [
- { name: '拆柜收货', icon: '卸', iconType: 'download', description: '待卸柜拍照签收,按目的类型扫码收货', available: true, badge: '真实接口' }
+ { key: 'inbound', name: '入仓作业', icon: '入', description: '单账号锁定一柜,完成柜号级入库闭环', operations: [
+ { name: '拆柜收货', icon: '卸', iconType: 'download', description: '三类照片签收,FBA 按 SKU 箱数、海外仓逐箱收货', available: true, badge: '真实接口' }
] },
{ key: 'inventory', name: '库内作业', icon: '库', description: '盘点、移库与托盘管理', operations: [
{ name: '库存盘点', icon: '盘', iconType: 'list', description: '按任务扫描库位和库存', available: true, badge: '原型' },
diff --git a/src/pages.json b/src/pages.json
index f9e33e7..5ce934d 100644
--- a/src/pages.json
+++ b/src/pages.json
@@ -3,7 +3,7 @@
{ "path": "pages/login/login", "style": { "navigationStyle": "custom", "navigationBarTitleText": "柏悦物流海外版", "backgroundColor": "#F5F7FA" } },
{ "path": "pages/home/home", "style": { "navigationStyle": "custom", "navigationBarTitleText": "海外仓工作台", "backgroundColor": "#F5F7FA" } },
{ "path": "pages/module/module", "style": { "navigationStyle": "custom", "navigationBarTitleText": "海外仓业务", "backgroundColor": "#F5F7FA" } },
- { "path": "pages/operation/operation", "style": { "navigationStyle": "custom", "navigationBarTitleText": "现场作业", "backgroundColor": "#F5F7FA" } },
+ { "path": "pages/operation/operation", "style": { "navigationStyle": "custom", "navigationBarTitleText": "柜号入库与现场作业", "backgroundColor": "#F5F7FA" } },
{ "path": "pages/history/history", "style": { "navigationStyle": "custom", "navigationBarTitleText": "作业记录", "backgroundColor": "#F5F7FA" } },
{ "path": "pages/profile/profile", "style": { "navigationStyle": "custom", "navigationBarTitleText": "个人中心", "backgroundColor": "#F5F7FA" } },
{ "path": "pages/scanner-test/scanner-test", "style": { "navigationStyle": "custom", "navigationBarTitleText": "广播扫码测试", "backgroundColor": "#F5F7FA" } }
diff --git a/src/pages/operation/operation.vue b/src/pages/operation/operation.vue
index cd6d90f..856fd82 100644
--- a/src/pages/operation/operation.vue
+++ b/src/pages/operation/operation.vue
@@ -88,7 +88,7 @@
本次已收 {{ lastReceipt.acceptedPackages }} 箱,{{ lastReceipt.inboundCode }} 剩余 {{ lastReceipt.pendingPackages }} 箱
-
+
@@ -113,6 +113,7 @@