diff --git a/AGENTS.md b/AGENTS.md index 8919130..28343fd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,6 +26,7 @@ - 提交成功或明确放弃后清理草稿;退出应用或设备重启后同一作业员、任务和柜次可恢复,其他账号不得继承。 - PDA 仅允许现场拍照;Admin 可通过独立受权入口人工上传。五类证据各至少 1 张有效照片,同类型允许多张追加;错误或重复照片标记作废并保留原因、操作人和时间,不硬删除。设备端证据查询、上传、预览和作废统一按当前 `taskId + warehouseId` 走任务专用 API,服务端再绑定真实柜 `containerBookingId`;不得调用旧入仓单/客户作用域证据接口。 - 与管理后台头程订单、仓库任务和 `baiyue-firstleg` Controller/OpenAPI 对齐稳定标识、状态机、时间和异常码,禁止在前端自行发明另一套业务状态。 +- 一件代发清点组托使用独立 `/first-leg/pda-dropship` 任务链:箱码先经只读识别再确认实收;系统托盘码可先于或晚于箱码扫描。`count` 携托盘码时直接绑定,箱码先扫则进入待绑托区并由 `bind-pending` 批量绑定;换托只使用服务端 `cartonId`,封托后按整托不可拆。只消费 `platform:firstleg:pda:view` 与 `platform:firstleg:pda:work` 权限结果,不在 PDA 自建权限语义。 ## 设备、安全与可用性 @@ -57,6 +58,7 @@ | 待处理任务、服务端就绪与领取 | `pages/tasks/tasks.vue` | `api/pda-loading.js` 的 `listLoadingOrders`、`getContainerPickingTask`、`claimContainerPickingTask`;合并任务和缺少柜号/仓库/版本的任务不可进入 | | 业务编号展示与错误脱敏 | `utils/user-facing.js`、`tests/user-facing-identifiers.test.mjs` | 任务和作业页优先显示真实集装箱号,缺失时显示柜次号,再缺失显示“集装箱号 —”;不得展示 taskId、inboundId、orderId、loadingPlanId、containerBookingId 或长整型内部 ID,错误仍保留 code、requestId、traceId | | 拣货任务与箱码扫码 | `pages/picking/`、`stores/picking.js` | `api/pda-loading.js`、`utils/scan-broadcast.js`、`components/ScanTargetInput.vue`;服务端契约见 `../../baiyue-firstleg/firstleg/src/main/resources/openapi/phase3-pda-convergence-v1.yaml` | +| 一件代发清点组托 | `pages/dropship-palletizing/tasks.vue`、`pages/dropship-palletizing/dropship-palletizing.vue` | `api/pda-dropship.js`、`stores/dropship-palletizing.js`、`utils/dropship-palletizing-state.js`;服务端契约见 `../../baiyue-firstleg/firstleg/src/main/resources/openapi/dropship-palletization-v1.yaml`,合同 `tests/dropship-palletizing-*.test.mjs` | | 装柜取证与完成 | `pages/picking/picking.vue` 的 `evidence` 阶段 | 正式边界是 `api/pda-loading.js` 的证据查询、预览、上传、作废和 `completeContainerPickingTask` 全部按当前任务与仓库进入;不得保留按入仓单/客户作用域的旧证据调用。上传队列 `utils/evidence-upload-queue.js`,合同 `tests/pda-loading-api.test.mjs`、`tests/picking-evidence-stage.test.mjs` | | 装柜取证与完成 | `pages/picking/picking.vue` 的 `evidence` 阶段 | 正式边界是 `api/pda-loading.js` 的证据查询、预览、上传、作废和 `completeContainerPickingTask` 全部按当前任务与仓库进入;不得保留按入仓单/客户作用域的旧证据调用。上传队列 `utils/evidence-upload-queue.js`,合同 `tests/pda-loading-api.test.mjs`、`tests/picking-evidence-stage.test.mjs`、`tests/seal-save-idempotency.test.mjs` | | 扫描诊断 | `pages/scanner-test/` | `utils/scan-broadcast.js` 与广播 Action/Extra | diff --git a/README.md b/README.md index e07f3b2..08270c8 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ 国内货柜履约装柜取证已具备接口适配基础:拣货页展示空仓、半仓、满仓、锁仓和带锁码照五类证据,经 File 服务上传取得真实 `fileId`,并展示待上传、上传中、失败、成功和重试状态。产品已确认 PDA 只能现场拍照,Admin 可人工上传;每类至少 1 张有效照片、同类型可多张追加,错误或重复照片只作废不硬删除。当前页面和上传队列仍按会话内单类型记录设计,尚未完整达到“多图追加+作废审计”契约。 -登录后默认进入首页,不再直接进入拣货。首页采用入库、出库、库内作业、查询四个分组的三列宫格,不设置常驻底部导航;作业记录收纳在左上角功能菜单,个人中心从右上角人员入口进入。第一版本只有“出库 → 集装箱拣货”可点击并具备完整本地状态交互,其余业务入口统一置灰并禁用,保留在界面中用于确认后续信息架构。 +登录后默认进入首页,不再直接进入拣货。首页采用入库、出库、库内作业、查询四个分组的三列宫格,不设置常驻底部导航;作业记录收纳在左上角功能菜单,个人中心从右上角人员入口进入。当前正式入口包括“出库 → 集装箱拣货”和“入库 → 一件代发清点组托”,其余业务入口统一置灰并禁用。 ## 页面与状态结构 @@ -20,6 +20,10 @@ - `pages/profile/profile`:人员、班次、仓库信息及退出登录; - `pages/scanner-test/scanner-test`:Android 硬件扫描广播诊断页; - `pages/picking/picking`:集装箱拣货完整交互。 +- `pages/dropship-palletizing/tasks`:一件代发待清点组托任务、领取与继续作业; +- `pages/dropship-palletizing/dropship-palletizing`:逐箱识别、SKU 实收确认、待绑托批量绑定、换托与封托。 + +一件代发清点组托只调用 Firstleg 的 `/first-leg/pda-dropship` 正式接口。箱码先经 `cartons/resolve` 只读识别,确认后才调用 `cartons/count`;先扫托盘时确认箱码直接绑定当前托盘,先扫多箱时箱码进入待绑托区,后扫系统托盘码通过 `pallets/bind-pending` 批量绑定。所有写操作要求在线并携带稳定幂等标识,冲突后刷新权威任务;封托后的托盘只读并作为后续分柜、装柜的不可拆单元。 登录会话集中在 `stores/session.js` 并持久化到设备,拣货会话集中在 `stores/picking.js`。认证请求由 `api/http.js` 统一处理:Access Token 临近过期时使用 Refresh Token 轮换;并发请求共享一次刷新;401 刷新成功后只重放一次;Refresh Token 明确失效时清理会话和作业草稿并返回登录页,网络或服务异常不会误清会话。产品契约要求未提交拣货草稿持久化到设备,并绑定作业员、任务和柜次,其他账号不能继承;成功提交或明确放弃后才清理。页面头部与底部主导航位于 `components/`,演示账号、模块和任务数据位于 `data/demo-data.js`,跨页面 PDA 视觉规则位于 `styles/pda.scss`。 diff --git a/api/pda-dropship.js b/api/pda-dropship.js new file mode 100644 index 0000000..609e061 --- /dev/null +++ b/api/pda-dropship.js @@ -0,0 +1,76 @@ +import { apiRequest } from '@/api/http.js' + +const ROOT = '/first-leg/pda-dropship' + +function requireValue(value, message) { + if (value === null || value === undefined || value === '') throw new Error(message) +} + +function operationHeader(operationId) { + return operationId ? { 'Idempotency-Key': String(operationId) } : {} +} + +export function listDropshipPalletizingTasks(keyword = '') { + const query = keyword ? `?keyword=${encodeURIComponent(String(keyword).trim())}` : '' + return apiRequest({ url: `${ROOT}/tasks/pending${query}`, method: 'GET' }) +} + +export function getDropshipPalletizingTask(taskId, warehouseId) { + requireValue(taskId, '缺少清点组托任务标识') + requireValue(warehouseId, '缺少作业仓库标识') + return apiRequest({ url: `${ROOT}/tasks/${encodeURIComponent(taskId)}?warehouseId=${encodeURIComponent(warehouseId)}`, method: 'GET' }) +} + +export function resolveDropshipCarton(taskId, warehouseId, boxCode) { + requireValue(taskId, '缺少清点组托任务标识') + requireValue(warehouseId, '缺少作业仓库标识') + requireValue(boxCode, '缺少箱码') + return apiRequest({ url: `${ROOT}/tasks/${encodeURIComponent(taskId)}/cartons/resolve?warehouseId=${encodeURIComponent(warehouseId)}&boxCode=${encodeURIComponent(String(boxCode).trim())}`, method: 'GET' }) +} + +export function claimDropshipPalletizingTask(taskId, warehouseId) { + requireValue(taskId, '缺少清点组托任务标识') + requireValue(warehouseId, '缺少作业仓库标识') + return apiRequest({ url: `${ROOT}/tasks/${encodeURIComponent(taskId)}/claim`, method: 'POST', data: { warehouseId } }) +} + +export function countDropshipCarton(taskId, payload = {}) { + requireValue(taskId, '缺少清点组托任务标识') + for (const [field, message] of [['warehouseId', '缺少作业仓库标识'], ['boxCode', '缺少箱码'], ['actualSkuQuantity', '缺少 SKU 实收数量'], ['discrepancyAction', '缺少差异处理动作'], ['operationId', '缺少操作幂等标识']]) requireValue(payload[field], message) + return apiRequest({ + url: `${ROOT}/tasks/${encodeURIComponent(taskId)}/cartons/count`, + method: 'POST', + header: operationHeader(payload.operationId), + data: { + warehouseId: payload.warehouseId, + boxCode: payload.boxCode, + actualSkuQuantity: payload.actualSkuQuantity, + discrepancyAction: payload.discrepancyAction, + discrepancyRemark: payload.discrepancyRemark || null, + palletNo: payload.palletNo || null, + operationId: payload.operationId + } + }) +} + +export function bindPendingDropshipCartons(taskId, payload = {}) { + for (const [field, message] of [['warehouseId', '缺少作业仓库标识'], ['palletNo', '缺少托盘码'], ['operationId', '缺少操作幂等标识']]) requireValue(payload[field], message) + return apiRequest({ url: `${ROOT}/tasks/${encodeURIComponent(taskId)}/pallets/bind-pending`, method: 'POST', header: operationHeader(payload.operationId), data: payload }) +} + +export function moveDropshipCarton(taskId, cartonId, payload = {}) { + requireValue(cartonId, '缺少箱明细标识') + for (const [field, message] of [['warehouseId', '缺少作业仓库标识'], ['operationId', '缺少操作幂等标识']]) requireValue(payload[field], message) + return apiRequest({ + url: `${ROOT}/tasks/${encodeURIComponent(taskId)}/cartons/${encodeURIComponent(cartonId)}/move`, + method: 'POST', + header: operationHeader(payload.operationId), + data: { warehouseId: payload.warehouseId, targetPalletNo: payload.targetPalletNo || null, operationId: payload.operationId } + }) +} + +export function sealDropshipPallet(taskId, palletId, payload = {}) { + requireValue(palletId, '缺少托盘标识') + for (const [field, message] of [['warehouseId', '缺少作业仓库标识'], ['operationId', '缺少操作幂等标识'], ['expectedVersion', '缺少托盘版本']]) requireValue(payload[field], message) + return apiRequest({ url: `${ROOT}/tasks/${encodeURIComponent(taskId)}/pallets/${encodeURIComponent(palletId)}/seal`, method: 'POST', header: operationHeader(payload.operationId), data: payload }) +} diff --git a/data/demo-data.js b/data/demo-data.js index ba2fdde..40abe27 100644 --- a/data/demo-data.js +++ b/data/demo-data.js @@ -7,13 +7,14 @@ export const DEMO_ACCOUNTS = [ ] export const MODULES = [ - { key: 'inbound', name: '入库', icon: '入', description: '收货、上架与质检', meta: '3 个静态页面', operations: [ + { key: 'inbound', name: '入库', icon: '入', description: '收货、上架与质检', meta: '1 个正式入口 · 3 个未开放', operations: [ + { name: '一件代发清点组托', icon: '托', description: '逐箱清点并按任意扫码顺序完成组托', available: true, route: '/pages/dropship-palletizing/tasks' }, { name: '到货收货', icon: '收', description: '扫描到货单并确认实收数量' }, { name: '商品上架', icon: '上', description: '按推荐库位完成商品上架' }, { name: '入库质检', icon: '检', description: '登记质检结果与异常信息' } ] }, { key: 'outbound', name: '出库', icon: '出', description: '拣货、复核与装箱', meta: '3 个待办', operations: [ - { name: '集装箱拣货', icon: '拣', description: '按箱号扫描并确认拣货', available: true }, + { name: '集装箱拣货', icon: '拣', description: '按箱号扫描并确认拣货', available: true, route: '/pages/tasks/tasks' }, { name: '出库复核', icon: '核', description: '复核拣货商品与出库数量' }, { name: '装箱交接', icon: '装', description: '完成装箱并登记交接信息' } ] }, diff --git a/pages.json b/pages.json index bada3f1..2f6ee23 100644 --- a/pages.json +++ b/pages.json @@ -8,7 +8,9 @@ { "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" } }, - { "path": "pages/picking/picking", "style": { "navigationStyle": "custom", "navigationBarTitleText": "集装箱拣货", "backgroundColor": "#F5F7FA" } } + { "path": "pages/picking/picking", "style": { "navigationStyle": "custom", "navigationBarTitleText": "集装箱拣货", "backgroundColor": "#F5F7FA" } }, + { "path": "pages/dropship-palletizing/tasks", "style": { "navigationStyle": "custom", "navigationBarTitleText": "一件代发任务", "backgroundColor": "#F5F7FA" } }, + { "path": "pages/dropship-palletizing/dropship-palletizing", "style": { "navigationStyle": "custom", "navigationBarTitleText": "一件代发清点组托", "backgroundColor": "#F5F7FA" } } ], "globalStyle": { "navigationBarTextStyle": "white", diff --git a/pages/dropship-palletizing/dropship-palletizing.vue b/pages/dropship-palletizing/dropship-palletizing.vue new file mode 100644 index 0000000..ba30bf1 --- /dev/null +++ b/pages/dropship-palletizing/dropship-palletizing.vue @@ -0,0 +1,81 @@ +