From bf760f1abd3d339f83858e8a781be436a5c4ff87 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Tue, 15 Sep 2026 15:20:43 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 8 +- src/api/relabel.js | 22 +++- src/data/overseas-workbench.js | 4 +- src/pages/operation/operation.vue | 2 +- src/pages/relabel/relabel.vue | 183 +++++++++++++++++++++----- tests/warehouse-workflows.test.mjs | 37 ++++++ tests/workbench-availability.test.mjs | 4 +- 7 files changed, 211 insertions(+), 49 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 42715b0..acb344c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -45,7 +45,7 @@ | 托盘管理真实作业 | `src/pages/pallet/pallet.vue`、`src/api/pallet.js` | 扫码查询托盘内容,按服务端返回的托盘/库存版本执行组托、拆托、换托和解绑;公网前缀 `/api/pda/v1/last-leg/pallet-operations/**` | | 库存盘点(入口隐藏) | `src/pages/stocktake/stocktake.vue`、`src/api/warehouse-operations.js`、`src/stores/warehouse-work.js` | 底层创建、恢复、计数与提交实现仍保留;当前 `src/data/overseas-workbench.js` 不注册库存盘点,不向现场开放入口 | | 商品移库真实作业 | `src/pages/move/move.vue`、`src/api/warehouse-operations.js`、`src/stores/warehouse-work.js` | 扫描来源托盘与目标库位执行同仓移库;冲突后重新读取服务端事实 | -| 换标真实作业 | `src/pages/relabel/relabel.vue`、`src/api/relabel.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` | 任务列表/详情、托盘业务码扫码拣货、幂等完成;发车必须同时完成拣货核平、平台转派送并严格消费服务端 `canDepart`、`departBlockReason`,不得按本地数量自行推断;公网前缀 `/api/pda/v1/last-leg/dispatches/**` | | 普通/一件代发出库 | `src/pages/outbound/outbound.vue`、`src/api/fulfillment.js`、`src/stores/fulfillment.js` | 任务列表/详情、稳定业务码扫码拣货和幂等完成;公网前缀 `/api/pda/v1/last-leg/outbound-tasks/**`;派送由 Admin/Client 承载 | @@ -56,6 +56,12 @@ ## 海外业务接口边界 +- 换标任务标明类型,仅显示实际明细需要的 Tab。混合明细同时进入箱唛/SKU 作业,服务端分开保存两部分进度,`cartonProcessedPackages/skuProcessedPackages` 决定各 Tab 待处理数量;两项均完成后整箱计数一次,不能仅扫箱唛就视为 SKU 完成。 + +- 换标完工提交使 PDA 任务进入 `WAIT_REVIEW`,申请进入 `CUSTOMER_CONFIRM_PENDING`,客户确认原子完成复核并释放后续履约资格。PDA 复核池只读;平台通过专用 `/rework` 接口携带任务版本、幂等键和必填备注退回,历史事实归档后重新计数,PDA 展示 `reviewRemark` 并清空上一轮输入和照片草稿。 + +- 换标执行完成照片以缩略图展示并支持大图预览、移除和重拍。重新进入任务通过 `relabel-tasks/{relabelNo}/photos/files/{fileId}/download` 鉴权读取,不持久化设备临时图片地址;重拍先上传成功再替换待提交文件 ID,失败保留原照片,移除只解绑待提交引用。 + - 退货查询空态集中在 `src/api/return-receipt.js`、`src/api/return-packing.js`:仅 `startReturnReceipt`、`findReturnSkuCandidates`、`findPackingOrders` 将 `A0404/404` 转为无匹配结果;签收确认、合箱写入和标签读取不得套用此处理,权限、网络与服务错误仍须保留诊断并报错。回归见 `tests/return-receipt-api.test.mjs`、`tests/return-packing-api.test.mjs`,不得将查询无记录误报为写入成功。 - `src/api/inbound.js` 已接入待卸柜候选、柜详情/prepare、current 恢复、三类照片签收、统一箱码定位入仓码、实物托盘解析、普通 FBA bulk/普通 OVERSEAS carton、一件代发 source pallet 整托收货及整柜完成;FBA bulk 不提交 `skuCode`,库存分配由服务端按计划明细完成;一件代发只提交国内冻结的来源托盘号,不允许逐箱或拆托收货。响应与 404/409 判定集中在 `src/api/receiving-adapter.js`,双 Token 刷新与海外 401 单次重放由 `src/api/overseas.js` 统一处理。 - 入仓摘要、箱码解析和实收响应的 `assigned/mergeAllowed/urgent/mergeHint` 只消费 Lastleg 返回值,布尔标志只有 `true` 生效,`mergeAllowed` 缺省为 `false`;合托仅作提示,不能改变收货资格或触发分配。首扫实物托盘只确认服务端已有托盘、库位与资源锁预留,不在设备端重新分配托盘、库位或资源锁;只提交业务箱码、托盘码及当前收货数量,最终资格由服务端决定。 diff --git a/src/api/relabel.js b/src/api/relabel.js index 1460d8d..b3312a8 100644 --- a/src/api/relabel.js +++ b/src/api/relabel.js @@ -1,4 +1,4 @@ -import { overseasRequest, overseasUpload } from './overseas.js' +import { overseasRequest, overseasUpload, overseasDownload } from './overseas.js' const BASE = '/pda/v1/last-leg/relabel-tasks' const encoded = (value) => encodeURIComponent(String(value || '').trim()) @@ -6,18 +6,22 @@ const encoded = (value) => encodeURIComponent(String(value || '').trim()) function asList(value) { return Array.isArray(value) ? value : [] } export function normalizeRelabel(value = {}) { + const items = asList(value.items).map((item) => ({ + ...item, itemId: String(item.itemId || ''), skuCode: String(item.skuCode || ''), + newSkuCode: String(item.newSkuCode || ''), operationType: String(item.operationType || 'CARTON_LABEL'), expectedPackages: Number(item.expectedPackages || 0), + relabelledPackages: Number(item.relabelledPackages || 0), skippedPackages: Number(item.skippedPackages || 0), + exceptionPackages: Number(item.exceptionPackages || 0) + })) + const inferredOperationType = items.some((item) => item.operationType === 'CARTON_AND_SKU') ? 'CARTON_AND_SKU' : items.some((item) => item.operationType === 'SKU_LABEL') + ? (items.some((item) => item.operationType !== 'SKU_LABEL') ? 'CARTON_AND_SKU' : 'SKU_LABEL') : 'CARTON_LABEL' return { ...value, relabelNo: String(value.relabelNo || ''), customerOrderNo: String(value.customerOrderNo || ''), version: Number(value.version || 0), expectedPackages: Number(value.expectedPackages || 0), + operationType: String(value.operationType || inferredOperationType), relabelledPackages: Number(value.relabelledPackages || 0), skippedPackages: Number(value.skippedPackages || 0), exceptionPackages: Number(value.exceptionPackages || 0), - items: asList(value.items).map((item) => ({ - ...item, itemId: String(item.itemId || ''), skuCode: String(item.skuCode || ''), - newSkuCode: String(item.newSkuCode || ''), expectedPackages: Number(item.expectedPackages || 0), - relabelledPackages: Number(item.relabelledPackages || 0), skippedPackages: Number(item.skippedPackages || 0), - exceptionPackages: Number(item.exceptionPackages || 0) - })), + items, unitFacts: asList(value.unitFacts), history: asList(value.history), completionPhotoFileIds: asList(value.completionPhotoFileIds).map(String) } @@ -36,9 +40,13 @@ async function detailAction(relabelNo, suffix, request) { } export const claimRelabelTask = (relabelNo, request) => detailAction(relabelNo, '/claim', request) +export const scanRelabelCarton = (relabelNo, request) => detailAction(relabelNo, '/cartons/scan', request) +export const listRelabelDocuments = (relabelNo) => overseasRequest('GET', `${BASE}/${encoded(relabelNo)}/documents`) +export const downloadRelabelDocument = (relabelNo, file) => overseasDownload(`${BASE}/${encoded(relabelNo)}/documents/${encoded(file.fileId)}/download?scope=${encoded(file.scope)}${file.itemId ? `&itemId=${encoded(file.itemId)}` : ''}`) export const relabelUnit = (relabelNo, request) => detailAction(relabelNo, '/units', request) export const skipRelabelUnit = (relabelNo, request) => detailAction(relabelNo, '/units/skip', request) export const reportRelabelException = (relabelNo, request) => detailAction(relabelNo, '/exceptions', request) export const completeRelabelTask = (relabelNo, request) => detailAction(relabelNo, '/complete', request) export const reviewRelabelTask = (relabelNo, request) => detailAction(relabelNo, '/review', request) export const uploadRelabelPhoto = (relabelNo, filePath) => overseasUpload(`${BASE}/${encoded(relabelNo)}/photos/files`, filePath) +export const downloadRelabelPhoto = (relabelNo, fileId) => overseasDownload(`${BASE}/${encoded(relabelNo)}/photos/files/${encoded(fileId)}/download`) diff --git a/src/data/overseas-workbench.js b/src/data/overseas-workbench.js index 1fd1eab..6ea3219 100644 --- a/src/data/overseas-workbench.js +++ b/src/data/overseas-workbench.js @@ -12,8 +12,8 @@ export const MODULES = [ { key: 'move', name: '商品移库', nameKey: 'modules.move.name', icon: '移', iconType: 'loop', description: '扫描来源与目标库位完成移库', descriptionKey: 'modules.move.description', available: true, route: '/pages/move/move' }, { key: 'pallet', name: '托盘管理', nameKey: 'modules.pallet.name', icon: '托', iconType: 'map', description: '扫码查询、组托、拆托、换托及解绑', descriptionKey: 'modules.pallet.description', available: true, route: '/pages/pallet/pallet' } ] }, - { key: 'relabel', name: '换标作业', nameKey: 'modules.relabel.name', icon: '换', description: '换标收货、执行与复核', descriptionKey: 'modules.relabel.description', operations: [ - { key: 'relabelReceive', name: '换标收货', nameKey: 'modules.relabelReceive.name', icon: '收', iconType: 'download', description: '扫描换标单并确认待处理库存', descriptionKey: 'modules.relabelReceive.description', available: true, route: '/pages/relabel/relabel?op=relabelReceive' }, + { key: 'relabel', name: '换标作业', nameKey: 'modules.relabel.name', icon: '换', description: '换标列表、执行与复核', descriptionKey: 'modules.relabel.description', operations: [ + { key: 'relabelReceive', name: '换标列表', nameKey: 'modules.relabelReceive.name', icon: '列', iconType: 'list', description: '选择换标单并直接进入执行', descriptionKey: 'modules.relabelReceive.description', available: true, route: '/pages/relabel/relabel?op=relabelReceive' }, { key: 'relabelExecute', name: '换标执行', nameKey: 'modules.relabelExecute.name', icon: '换', iconType: 'compose', description: '扫描旧标和新标记录换标结果', descriptionKey: 'modules.relabelExecute.description', available: true, route: '/pages/relabel/relabel?op=relabelExecute' }, { key: 'relabelReview', name: '换标复核', nameKey: 'modules.relabelReview.name', icon: '核', iconType: 'checkbox', description: '复核标签和商品关联关系', descriptionKey: 'modules.relabelReview.description', available: true, route: '/pages/relabel/relabel?op=relabelReview' } ] }, diff --git a/src/pages/operation/operation.vue b/src/pages/operation/operation.vue index 4fd8e5d..0acdf28 100644 --- a/src/pages/operation/operation.vue +++ b/src/pages/operation/operation.vue @@ -143,7 +143,7 @@ pageTitle() { return { candidates: '待卸货集装箱', detail: '柜号详情', sign: '拆柜拍照签收', receive: '当前柜收货', completed: '入库完成' }[this.phase] }, pageSubtitle() { return { candidates: '选择集装箱后拍摄 3 张照片完成签收', detail: '核对柜号与整柜计划箱数', sign: '完成现场取证后进入收货', receive: '扫描箱码定位入仓码后开始收货', completed: '库存与柜级进度已由服务端核平' }[this.phase] } }, - onLoad(options) { this.moduleKey = options.module || 'query'; const key = options.op || (this.moduleKey === 'inbound' ? 'receiving' : 'inventoryQuery'); this.operationName = ({ receiving: '拆柜收货', stocktake: '库存盘点', move: '商品移库', pallet: '托盘管理', relabelReceive: '换标收货', relabelExecute: '换标执行', relabelReview: '换标复核', inventoryQuery: '库存查询', locationQuery: '库位查询', palletQuery: '托盘查询' })[key] || '库存查询' }, + onLoad(options) { this.moduleKey = options.module || 'query'; const key = options.op || (this.moduleKey === 'inbound' ? 'receiving' : 'inventoryQuery'); this.operationName = ({ receiving: '拆柜收货', stocktake: '库存盘点', move: '商品移库', pallet: '托盘管理', relabelReceive: '换标列表', relabelExecute: '换标执行', relabelReview: '换标复核', inventoryQuery: '库存查询', locationQuery: '库位查询', palletQuery: '托盘查询' })[key] || '库存查询' }, onShow() { if (!requireSession()) return; this.user = session.currentUser; this.startScanner(); if (this.inboundMode && (!this.bootstrapped || this.phase === 'candidates')) this.bootstrap() }, onHide() { this.stopScanner() }, onUnload() { this.stopScanner() }, beforeUnmount() { clearTimeout(this._toastTimer); this.stopScanner() }, methods: { diff --git a/src/pages/relabel/relabel.vue b/src/pages/relabel/relabel.vue index 90230b1..99088f2 100644 --- a/src/pages/relabel/relabel.vue +++ b/src/pages/relabel/relabel.vue @@ -2,36 +2,62 @@ - {{ title }}任务和仓库范围由服务端按当前 PDA Token 校验;扫描与人工输入使用同一正式接口 - + {{ title }}{{ stageSubtitle }} + - {{ candidateHeading }} + {{ candidateHeading }} 正在加载换标任务… {{ emptyText }} - {{ item.relabelNo }}客户订单:{{ item.customerOrderNo || '—' }}{{ statusLabel(item.taskStatus || item.applicationStatus) }}{{ item.expectedPackages }}应处理{{ item.relabelledPackages }}已换标{{ item.skippedPackages + item.exceptionPackages }}跳过/异常详情 › + {{ item.relabelNo }}客户订单:{{ item.customerOrderNo || '—' }}换标类型:{{ relabelTypeName(item.operationType) }}{{ statusLabel(item.taskStatus || item.applicationStatus) }}{{ item.expectedPackages }}应处理{{ item.relabelledPackages }}已换标{{ item.skippedPackages + item.exceptionPackages }}跳过/异常执行 › + {{ item.relabelNo }}客户订单:{{ item.customerOrderNo || '—' }}换标类型:{{ relabelTypeName(item.operationType) }}{{ statusLabel(item.taskStatus || item.applicationStatus) }}{{ item.expectedPackages }}应处理{{ item.relabelledPackages }}已换标{{ item.skippedPackages + item.exceptionPackages }}跳过/异常 - + + 换标类型:{{ relabelTypeName(current.operationType) }} {{ current.relabelNo }}客户订单:{{ current.customerOrderNo || '—' }}标签截止:{{ formatTime(current.labelDeadline) }}{{ statusLabel(current.taskStatus || current.applicationStatus) }} {{ current.expectedPackages }}应处理{{ current.relabelledPackages }}已换标{{ current.skippedPackages }}跳过{{ current.exceptionPackages }}异常 - - -