From 1b129c4cdb5d18c8d928813544abcffcb32ddf9b Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 24 Aug 2026 19:23:23 +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 --- src/api/inbound.js | 26 ++-- src/api/overseas.js | 32 +++-- src/data/overseas-workbench.js | 6 +- src/pages/module/module.vue | 2 +- src/pages/operation/operation.vue | 203 ++++++++++++++++++++++++------ src/stores/receiving.js | 64 ++++++++++ 6 files changed, 264 insertions(+), 69 deletions(-) create mode 100644 src/stores/receiving.js diff --git a/src/api/inbound.js b/src/api/inbound.js index 972aa52..e82bbc4 100644 --- a/src/api/inbound.js +++ b/src/api/inbound.js @@ -1,15 +1,17 @@ import { overseasRequest, overseasUpload } from '@/api/overseas.js' -function scope(customerId) { return `?customerId=${encodeURIComponent(customerId)}` } -function base(planNo) { return `/pda/v1/last-leg/inbound-plans/${encodeURIComponent(planNo)}` } +const RECEIVING_BASE = '/pda/v1/last-leg/receiving-containers' +const current = (suffix = '') => `${RECEIVING_BASE}/current${suffix}` -export const getInboundTask = (planNo, customerId) => overseasRequest('GET', `${base(planNo)}${scope(customerId)}`) -export const pushInboundTask = (planNo, customerId, request) => overseasRequest('POST', `${base(planNo)}/push${scope(customerId)}`, request) -export const assignInboundPallet = (planNo, customerId, planPalletId, request) => overseasRequest('POST', `${base(planNo)}/pallets/${encodeURIComponent(planPalletId)}/assign${scope(customerId)}`, request) -export const countInboundPackages = (planNo, customerId, allocationId, request) => overseasRequest('POST', `${base(planNo)}/allocations/${encodeURIComponent(allocationId)}/count${scope(customerId)}`, request) -export const pickInboundPackages = (planNo, customerId, allocationId, request) => overseasRequest('POST', `${base(planNo)}/allocations/${encodeURIComponent(allocationId)}/pick${scope(customerId)}`, request) -export const scanInboundCarton = (planNo, customerId, request) => overseasRequest('POST', `${base(planNo)}/cartons/scan${scope(customerId)}`, request) -export const removeInboundCarton = (planNo, customerId, request) => overseasRequest('POST', `${base(planNo)}/cartons/remove${scope(customerId)}`, request) -export const bindUnloadPhotos = (planNo, customerId, request) => overseasRequest('POST', `${base(planNo)}/unload-photos${scope(customerId)}`, request) -export const completeInboundTask = (planNo, customerId) => overseasRequest('POST', `${base(planNo)}/complete${scope(customerId)}`) -export const uploadUnloadPhotoFile = (planNo, customerId, filePath) => overseasUpload(`${base(planNo)}/unload-photos/files${scope(customerId)}`, filePath) +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 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 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) diff --git a/src/api/overseas.js b/src/api/overseas.js index d3601d4..8968cb9 100644 --- a/src/api/overseas.js +++ b/src/api/overseas.js @@ -17,6 +17,10 @@ function unauthorized(response) { return response?.statusCode === 401 || code === 'A0401' } +function successful(response, body) { + return response?.statusCode === 200 && ['00000', '200'].includes(String(body?.code)) +} + async function refreshOverseasToken() { if (!session.refreshToken) throw new Error('登录状态已过期,请重新登录') if (!refreshPromise) { @@ -46,26 +50,32 @@ export async function overseasRequest(method, path, data, retry = true) { response = await rawRequest(method, path, data, session.token) } const body = response?.data || {} - if (response.statusCode !== 200 || String(body.code) !== '200') throw errorFrom(response, '海外作业请求失败') + if (!successful(response, body)) throw errorFrom(response, '海外作业请求失败') return body.data } -export function overseasUpload(path, filePath, name = 'file') { +function rawUpload(path, filePath, name, token) { return new Promise((resolve, reject) => { uni.uploadFile({ url: `${session.apiBase}${path}`, filePath, name, - header: { Authorization: `Bearer ${session.token}` }, - success(response) { - let body = null - try { body = JSON.parse(response.data || '{}') } catch (_) { /* handled below */ } - if (response.statusCode !== 200 || String(body?.code) !== '200') { - reject(errorFrom({ ...response, data: body }, '卸柜照片上传失败')); return - } - resolve(body.data) - }, + header: { Authorization: `Bearer ${token}` }, + success: resolve, fail: (cause) => reject(Object.assign(new Error(cause.errMsg || '卸柜照片上传失败'), { network: true })) }) }) } + +export async function overseasUpload(path, filePath, name = 'file', retry = true) { + let response = await rawUpload(path, filePath, name, session.token) + let body = null + try { body = JSON.parse(response.data || '{}') } catch (_) { /* handled below */ } + if (retry && unauthorized({ ...response, data: body })) { + await refreshOverseasToken() + response = await rawUpload(path, filePath, name, session.token) + try { body = JSON.parse(response.data || '{}') } catch (_) { body = null } + } + if (!successful(response, body)) throw errorFrom({ ...response, data: body }, '卸柜照片上传失败') + return body.data +} diff --git a/src/data/overseas-workbench.js b/src/data/overseas-workbench.js index d474925..675cc23 100644 --- a/src/data/overseas-workbench.js +++ b/src/data/overseas-workbench.js @@ -1,8 +1,6 @@ export const MODULES = [ - { key: 'inbound', name: '入仓作业', icon: '入', description: '到仓、卸柜收货与上架', operations: [ - { name: '到仓登记', icon: '到', iconType: 'download', description: '扫描入仓计划并登记车辆到仓', available: true, badge: '已接入' }, - { name: '卸柜收货', icon: '卸', iconType: 'paperplane', description: '按箱或托盘确认卸柜实收', available: true, badge: '已接入' }, - { name: '入仓上架', icon: '上', iconType: 'upload', description: '扫描容器与目标库位完成上架', available: true, badge: '已接入' } + { key: 'inbound', name: '入仓作业', icon: '入', description: '柜号签收、卸柜收货与入库', operations: [ + { name: '拆柜收货', icon: '卸', iconType: 'download', description: '待卸柜拍照签收,按目的类型扫码收货', available: true, badge: '真实接口' } ] }, { key: 'inventory', name: '库内作业', icon: '库', description: '盘点、移库与托盘管理', operations: [ { name: '库存盘点', icon: '盘', iconType: 'list', description: '按任务扫描库位和库存', available: true, badge: '原型' }, diff --git a/src/pages/module/module.vue b/src/pages/module/module.vue index ccdba48..6c9e967 100644 --- a/src/pages/module/module.vue +++ b/src/pages/module/module.vue @@ -5,7 +5,7 @@ {{ currentModule.icon }}{{ currentModule.name }}{{ currentModule.description }} 业务功能 - 当前为海外仓独立工程首版原型;真实作业接口统一经海外网关接入。 + 拆柜收货已经海外网关接入真实接口;标记“原型”的库内与换标功能不会模拟提交成功。 diff --git a/src/pages/operation/operation.vue b/src/pages/operation/operation.vue index 9a9e529..cd6d90f 100644 --- a/src/pages/operation/operation.vue +++ b/src/pages/operation/operation.vue @@ -1,61 +1,182 @@ diff --git a/src/stores/receiving.js b/src/stores/receiving.js new file mode 100644 index 0000000..cdc5bcc --- /dev/null +++ b/src/stores/receiving.js @@ -0,0 +1,64 @@ +import { reactive } from 'vue' + +export const receiving = reactive({ + candidates: [], selected: null, current: null, photoSlots: [null, null, null], + selectedInboundCode: '', resolvedCarton: null, resolvedSku: null, resolvedPallet: null, + activeScanTarget: 'container', lastReceipt: null +}) + +const idempotencyKeys = new Map() + +export function receivingPhase(session = receiving.current) { + if (!session) return 'candidates' + if (session.status === 'WAIT_SIGN') return 'sign' + if (session.status === 'SIGNED' || session.status === 'RECEIVING') return 'receive' + if (session.status === 'COMPLETED') return 'completed' + return 'candidates' +} + +export function receivingProgress(session = receiving.current) { + const total = Math.max(0, Number(session?.totalPackages || 0)) + const received = Math.max(0, Number(session?.receivedPackages || 0)) + return { total, received, pending: Math.max(0, Number(session?.pendingPackages ?? total - received)), percent: total ? Math.min(100, Math.round(received / total * 100)) : 0 } +} + +export function selectedInbound(session = receiving.current) { + return (session?.inbounds || []).find((item) => item.inboundCode === receiving.selectedInboundCode) || null +} + +export function setCurrentReceiving(value) { + receiving.current = value || null + receiving.selected = value || null + const chosen = selectedInbound(value) + if (!chosen || Number(chosen.pendingPackages || 0) <= 0) { + receiving.selectedInboundCode = (value?.inbounds || []).find((item) => Number(item.pendingPackages || 0) > 0)?.inboundCode || value?.inbounds?.[0]?.inboundCode || '' + } + if (value?.photos?.length) { + const roles = ['LICENSE_PLATE', 'CONTAINER_SEAL', 'DOOR_OPEN'] + receiving.photoSlots = roles.map((role) => { + const photo = value.photos.find((item) => item.role === role) + return photo ? { ...photo, fileId: String(photo.fileId), role, uploaded: true } : null + }) + } +} + +export function resetReceivingDraft({ keepCurrent = false } = {}) { + receiving.selected = keepCurrent ? receiving.current : null + if (!keepCurrent) receiving.current = null + receiving.photoSlots = [null, null, null] + receiving.selectedInboundCode = '' + receiving.resolvedCarton = null + receiving.resolvedSku = null + receiving.resolvedPallet = null + receiving.activeScanTarget = keepCurrent ? 'pallet' : 'container' + receiving.lastReceipt = null + idempotencyKeys.clear() +} + +export function idempotencyKey(action, payload = {}) { + const signature = `${action}:${JSON.stringify(payload)}` + if (!idempotencyKeys.has(signature)) idempotencyKeys.set(signature, `pda-${Date.now()}-${Math.random().toString(16).slice(2)}`) + return { signature, value: idempotencyKeys.get(signature) } +} + +export function releaseIdempotencyKey(signature) { idempotencyKeys.delete(signature) }