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 @@
-
-
-
- {{ operationName }}{{ inboundMode ? '海外仓真实入仓作业' : prototype.subtitle }}{{ inboundMode ? '真实接口' : '静态原型' }}
+
+
+
- 任务范围
- {{ task.taskNo }}计划:{{ task.planNo }}状态:{{ task.status }}卸柜照片:{{ task.unloadPhotoFileIds?.length || 0 }} 张预计报空:{{ task.estimatedEmptyReturnAt }}
- 托盘资源分配
- 卸柜实收
- 卸柜照片与完成已上传文件ID:{{ photoFileIds.join('、') }}
- 最近结果{{ lastResult }}{{ error.message }}code={{ error.code }}requestId={{ error.requestId }}traceId={{ error.traceId }}
+
+ {{ pageTitle }}{{ pageSubtitle }}
+ 真实接口
+
+
+ 正在恢复当前柜作业…
+
+
+
+ 扫描柜号
+
+
+
+ {{ candidates.length }} 个待卸货集装箱
+ 暂无待卸货集装箱
+
+ {{ item.containerNo }}{{ item.inboundCodeCount }} 个入仓码 · {{ item.planCount }} 个计划 · {{ item.plannedPalletCount }} 托目的仓:{{ item.destinationCodes.join(' / ') }}{{ statusLabel(item.status) }}
+ 收货进度{{ item.receivedPackages }} / {{ item.totalPackages }} 箱
+
+
+
+
+
+
+
+
+ 选择后账号将锁定当前柜,完成前不能切换其他柜号。
+
+
+
+
+
+
+ 卸货签收照片需完成 3 个现场照片槽位{{ uploadedPhotoCount }} / 3
+
+
+
+
+
+
+
+
+
+
+
+ 选择入仓码
+
+
+
+
+
+
+
+ {{ activeInbound.destinationType === 'FBA' ? 'FBA 按箱数收货' : '海外仓逐箱扫码收货' }}
+ {{ destinationLabel(activeInbound) }}
+
+
+ 1. 扫描箱唛
+
+
+ {{ resolvedCarton.cartonMark }}{{ resolvedCarton.skuCode || '无 SKU' }} · {{ resolvedCarton.inboundCode }}
+
+
+ 1. 扫描 SKU
+
+
+ {{ resolvedSku.skuCode }}{{ resolvedSku.inboundCode }} · 待收 {{ resolvedSku.pendingPackages ?? resolvedSku.totalPackages ?? '—' }} 箱
+
+ 2. 扫描实物托盘码
+
+
+ {{ resolvedPallet.palletCode }}{{ resolvedPallet.compatible ? '目的仓匹配,可收货' : '托盘不匹配' }}
+
+ 3. 输入本次箱数
+
+
+
+
+
+ 本次已收 {{ lastReceipt.acceptedPackages }} 箱,{{ lastReceipt.inboundCode }} 剩余 {{ lastReceipt.pendingPackages }} 箱
+
+
+
+
+ ✓当前柜已完成入库{{ current?.containerNo }}实收 {{ current?.receivedPackages }} / {{ current?.totalPackages }} 箱
+
+
+
+ {{ error.message }}
+ code={{ error.code }}requestId={{ error.requestId }}traceId={{ error.traceId }}
+
- {{ prototype.scanLabel }}该库内模块仍待后续真实接口接入
- {{ toast.message }}
+
+
+ {{ operationName }}{{ prototype.subtitle }}静态原型
+ 该库内模块尚未接入真实业务接口,不会模拟提交成功。
+
+
+ {{ toast.message }}
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) }