From 991150e64b2cad503478d70a0c250b6db020d450 Mon Sep 17 00:00:00 2001
From: ouyangxq <775153912@qq.com>
Date: Fri, 11 Sep 2026 11:15:07 +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 | 5 +-
api/pda-dropship.js | 41 ++++++
data/demo-data.js | 1 +
pages.json | 2 +
.../dropship-palletization.vue | 138 ++++++++++++++++++
pages/dropship-tasks/dropship-tasks.vue | 86 +++++++++++
pages/home/home.vue | 8 +-
pages/module/module.vue | 8 +-
tests/dropship-palletization.test.mjs | 44 ++++++
tests/page-contract.test.mjs | 8 +-
10 files changed, 332 insertions(+), 9 deletions(-)
create mode 100644 api/pda-dropship.js
create mode 100644 pages/dropship-palletization/dropship-palletization.vue
create mode 100644 pages/dropship-tasks/dropship-tasks.vue
create mode 100644 tests/dropship-palletization.test.mjs
diff --git a/AGENTS.md b/AGENTS.md
index 8919130..65e88fb 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -8,7 +8,7 @@
## 开发与发布边界
-- 当前已有 uni-app Vue 3 页面、`uni_modules/`、`vite.config.js`、`.env.example` 和 H5 同源代理配置。项目仍没有根级依赖清单、仓库内 npm 构建命令或可核实的 Android 构建命令;H5 已使用 HBuilderX 5.15 自带编译器和 `cli.exe launch web` 验证,具体命令见 `README.md`。现有 Node.js 内置测试仅覆盖页面契约与本地状态规则。
+- 当前已有 uni-app Vue 3 页面、`uni_modules/`、`vite.config.js`、`.env.example` 和 H5 同源代理配置。真实作业包括一件代发合托和集装箱拣货;项目仍没有根级依赖清单、仓库内 npm 构建命令或可核实的 Android 构建命令。H5 已使用 HBuilderX 5.15 自带编译器和 `cli.exe launch web` 验证,具体命令见 `README.md`。现有 Node.js 内置测试仅覆盖页面契约与本地状态规则。
- PDA 采用 uni-app 开发并支持在线更新;Android 硬件扫描已确认使用广播模式并已接入:Action=`android.intent.action.SCANRESULT`、字符串 Extra=`value`。设备型号不构成开发、发布或验收门禁;继续通过设备适配层接入,不绑定单一厂商 SDK。
- 正式作业已确认为在线模式:断网不能开始正式拣货、提交、完成任务或推进 `LOADED`,不实现离线业务提交队列;恢复网络后提示员工刷新重试。
- H5 调试使用 HBuilderX 5.15 自带的 uni-app Vue 3 编译器;Android 发布流程仍待明确后补充准确的依赖版本、打包命令,并同步更新 `README.md` 与本文件。
@@ -55,6 +55,7 @@
| 登录与会话 | `pages/login/`、`stores/session.js` | 远程鉴权适配和会话清理 |
| 工作台与模块入口 | `pages/home/`、`pages/module/`、`pages/operation/` | `data/demo-data.js` 与页面禁用规则 |
| 待处理任务、服务端就绪与领取 | `pages/tasks/tasks.vue` | `api/pda-loading.js` 的 `listLoadingOrders`、`getContainerPickingTask`、`claimContainerPickingTask`;合并任务和缺少柜号/仓库/版本的任务不可进入 |
+| 一件代发合托 | `pages/dropship-tasks/dropship-tasks.vue`、`pages/dropship-palletization/dropship-palletization.vue` | `api/pda-dropship.js`;测量完成后独立任务按箱码清点,可绑定既有 DP 托盘或系统发新托盘,全部归托后逐托封托;公网前缀 `/api/platform/v1/first-leg/pda-dropship/**` |
| 业务编号展示与错误脱敏 | `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/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` |
@@ -69,7 +70,7 @@
| 验收域 | PDA 范围与代码入口 | 验收首读 |
|---|---|---|---|
-| 作业全链路 | 登录、工作台、扫码拣货、装柜证据、草稿持久化、批量提交、异常和在线更新 | `pages/login/`、`pages/home/`、`pages/picking/`、`stores/session.js`、`stores/picking.js`、`utils/scan-broadcast.js`、`pages.json` | 按业务选择 `../../docs/订单与客户/`、`../../docs/入库测量与计费/`、`../../docs/国内货柜履约/` 和 `README.md` 的契约与验收规则 |
+| 作业全链路 | 登录、工作台、一件代发扫码清点/归托/封托、集装箱扫码拣货、装柜证据、草稿持久化、批量提交、异常和在线更新 | `pages/login/`、`pages/home/`、`pages/dropship-tasks/`、`pages/dropship-palletization/`、`pages/picking/`、`stores/session.js`、`stores/picking.js`、`utils/scan-broadcast.js`、`pages.json` | 按业务选择 `../../docs/订单与客户/`、`../../docs/入库测量与计费/`、`../../docs/国内货柜履约/` 和 `README.md` 的契约与验收规则 |
作业边界:PDA 不承担客户工作台或后台配置;服务端状态、权限、幂等和 `LOADED` 门禁是验收事实来源。发布采用既定 uni-app 在线更新安排。
diff --git a/api/pda-dropship.js b/api/pda-dropship.js
new file mode 100644
index 0000000..52ed010
--- /dev/null
+++ b/api/pda-dropship.js
@@ -0,0 +1,41 @@
+import { apiRequest } from '@/api/http.js'
+
+const ROOT = '/first-leg/pda-dropship'
+
+function operationId(action, taskId) {
+ const random = Math.random().toString(36).slice(2, 10)
+ return `pda-dropship-${action}-${taskId}-${Date.now()}-${random}`.slice(0, 128)
+}
+
+export function listDropshipPalletizationTasks(keyword = '') {
+ const query = String(keyword || '').trim()
+ return apiRequest({ url: `${ROOT}/tasks/pending${query ? `?keyword=${encodeURIComponent(query)}` : ''}`, method: 'GET' })
+}
+
+export function getDropshipPalletizationTask(taskId, warehouseId) {
+ return apiRequest({ url: `${ROOT}/tasks/${encodeURIComponent(taskId)}?warehouseId=${encodeURIComponent(warehouseId)}`, method: 'GET' })
+}
+
+export function claimDropshipPalletizationTask(taskId, warehouseId) {
+ return apiRequest({ url: `${ROOT}/tasks/${encodeURIComponent(taskId)}/claim`, method: 'POST', data: { warehouseId } })
+}
+
+export function resolveDropshipCarton(taskId, warehouseId, boxCode) {
+ return apiRequest({ url: `${ROOT}/tasks/${encodeURIComponent(taskId)}/cartons/resolve?warehouseId=${encodeURIComponent(warehouseId)}&boxCode=${encodeURIComponent(String(boxCode || '').trim())}`, method: 'GET' })
+}
+
+export function countDropshipCarton(taskId, payload) {
+ return apiRequest({ url: `${ROOT}/tasks/${encodeURIComponent(taskId)}/cartons/count`, method: 'POST', data: { ...payload, operationId: payload.operationId || operationId('count', taskId) } })
+}
+
+export function bindDropshipPendingCartons(taskId, warehouseId, palletNo) {
+ return apiRequest({ url: `${ROOT}/tasks/${encodeURIComponent(taskId)}/pallets/bind-pending`, method: 'POST', data: { warehouseId, palletNo: String(palletNo || '').trim().toUpperCase(), operationId: operationId('bind', taskId) } })
+}
+
+export function createAndBindDropshipPallet(taskId, warehouseId) {
+ return apiRequest({ url: `${ROOT}/tasks/${encodeURIComponent(taskId)}/pallets/create-and-bind`, method: 'POST', data: { warehouseId, operationId: operationId('create-bind', taskId) } })
+}
+
+export function sealDropshipPallet(taskId, warehouseId, pallet) {
+ return apiRequest({ url: `${ROOT}/tasks/${encodeURIComponent(taskId)}/pallets/${encodeURIComponent(pallet.palletId)}/seal`, method: 'POST', data: { warehouseId, expectedVersion: pallet.version, operationId: operationId('seal', taskId) } })
+}
diff --git a/data/demo-data.js b/data/demo-data.js
index ba2fdde..d3309fe 100644
--- a/data/demo-data.js
+++ b/data/demo-data.js
@@ -13,6 +13,7 @@ export const MODULES = [
{ name: '入库质检', icon: '检', description: '登记质检结果与异常信息' }
] },
{ key: 'outbound', name: '出库', icon: '出', description: '拣货、复核与装箱', meta: '3 个待办', operations: [
+ { name: '一件代发合托', icon: '托', description: '逐箱清点并生成或绑定 DP 托盘', available: true },
{ name: '集装箱拣货', icon: '拣', description: '按箱号扫描并确认拣货', available: true },
{ name: '出库复核', icon: '核', description: '复核拣货商品与出库数量' },
{ name: '装箱交接', icon: '装', description: '完成装箱并登记交接信息' }
diff --git a/pages.json b/pages.json
index bada3f1..80ca699 100644
--- a/pages.json
+++ b/pages.json
@@ -5,6 +5,8 @@
{ "path": "pages/module/module", "style": { "navigationStyle": "custom", "navigationBarTitleText": "业务模块", "backgroundColor": "#F5F7FA" } },
{ "path": "pages/operation/operation", "style": { "navigationStyle": "custom", "navigationBarTitleText": "现场作业", "backgroundColor": "#F5F7FA" } },
{ "path": "pages/tasks/tasks", "style": { "navigationStyle": "custom", "navigationBarTitleText": "拣货任务", "backgroundColor": "#F5F7FA" } },
+ { "path": "pages/dropship-tasks/dropship-tasks", "style": { "navigationStyle": "custom", "navigationBarTitleText": "一件代发合托", "backgroundColor": "#F5F7FA" } },
+ { "path": "pages/dropship-palletization/dropship-palletization", "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/pages/dropship-palletization/dropship-palletization.vue b/pages/dropship-palletization/dropship-palletization.vue
new file mode 100644
index 0000000..7b782ef
--- /dev/null
+++ b/pages/dropship-palletization/dropship-palletization.vue
@@ -0,0 +1,138 @@
+
+
+
+
+ {{ error }}
+ {{ notice }}
+ 正在加载合托任务…
+
+
+ 入仓码{{ task.inboundCode || '—' }}{{ statusLabel }}
+ {{ task.customerName || '客户未关联' }} · {{ task.warehouseName || '仓库未关联' }}
+
+ {{ task.totalCartons || 0 }}总箱
+ {{ task.countedCartons || 0 }}已清点
+ {{ task.boundCartons || 0 }}已归托
+ {{ task.unboundCartons || 0 }}待归托
+
+
+
+
+
+ 1. 扫箱清点
+ 扫描箱唛,核对 SKU 与本箱实际件数。
+
+
+ {{ carton.boxCode }}{{ carton.countStatus === 'UNCOUNTED' ? '待清点' : '已处理' }}
+ {{ carton.skuCode || 'SKU —' }} · {{ carton.cargoName || '品名 —' }}
+ 计划 {{ carton.expectedSkuQuantity || 0 }} 件
+ 实际件数
+ 差异说明
+
+
+
+
+
+ 2. 归托
+ 把已清点待归托箱绑定到现有 DP 托盘,或由系统生成新托盘。
+
+
+
+
+
+
+ 3. 托盘与封托所有箱清点并归托后,逐托确认封托。
+ 暂无托盘
+
+ {{ pallet.palletNo }}{{ pallet.status === 'SEALED' ? '已封托' : '待封托' }}
+ {{ pallet.cartonCount || 0 }} 箱 · {{ pallet.skuCount || 0 }} SKU · {{ pallet.weightKg || 0 }} kg · {{ pallet.volumeCbm || 0 }} m³
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/dropship-tasks/dropship-tasks.vue b/pages/dropship-tasks/dropship-tasks.vue
new file mode 100644
index 0000000..c223ee2
--- /dev/null
+++ b/pages/dropship-tasks/dropship-tasks.vue
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+ 待合托任务{{ loading ? '正在同步' : `共 ${tasks.length} 个任务` }}
+
+
+ {{ error }}
+ 暂无待合托任务
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/home/home.vue b/pages/home/home.vue
index fa74fef..7e69762 100644
--- a/pages/home/home.vue
+++ b/pages/home/home.vue
@@ -24,7 +24,7 @@
const ICON_TYPES = {
'到货收货': 'download', '商品上架': 'upload', '入库质检': 'checkbox',
- '集装箱拣货': 'cart', '出库复核': 'compose', '装箱交接': 'paperplane',
+ '一件代发合托': 'map', '集装箱拣货': 'cart', '出库复核': 'compose', '装箱交接': 'paperplane',
'库存盘点': 'list', '商品移库': 'loop', '库存调整': 'compose',
'商品查询': 'search', '库位查询': 'location', '任务查询': 'flag'
}
@@ -46,7 +46,11 @@
return true
},
methods: {
- openOperation(operation) { if (operation.available) uni.navigateTo({ url: operation.name === '集装箱拣货' ? '/pages/tasks/tasks' : '/pages/operation/operation' }) }
+ openOperation(operation) {
+ if (!operation.available) return
+ const routes = { '一件代发合托': '/pages/dropship-tasks/dropship-tasks', '集装箱拣货': '/pages/tasks/tasks' }
+ uni.navigateTo({ url: routes[operation.name] || '/pages/operation/operation' })
+ }
}
}
diff --git a/pages/module/module.vue b/pages/module/module.vue
index f36d69a..36120de 100644
--- a/pages/module/module.vue
+++ b/pages/module/module.vue
@@ -5,7 +5,7 @@
{{ currentModule.icon }}{{ currentModule.name }}{{ currentModule.description }}
业务功能
- 第一版本仅开放“出库 → 集装箱拣货”,其他业务将在后续版本接入。
+ 当前开放“一件代发合托”和“集装箱拣货”两条真实作业流程。
@@ -22,7 +22,11 @@
onLoad(options) { this.moduleKey = options.key || 'outbound' },
onShow() { if (!requireSession()) return; this.user = session.currentUser; this.overview = getPickingOverview() },
methods: {
- openOperation(operation) { if (operation.available) uni.navigateTo({ url: operation.name === '集装箱拣货' ? '/pages/tasks/tasks' : '/pages/operation/operation' }) }
+ openOperation(operation) {
+ if (!operation.available) return
+ const routes = { '一件代发合托': '/pages/dropship-tasks/dropship-tasks', '集装箱拣货': '/pages/tasks/tasks' }
+ uni.navigateTo({ url: routes[operation.name] || '/pages/operation/operation' })
+ }
}
}
diff --git a/tests/dropship-palletization.test.mjs b/tests/dropship-palletization.test.mjs
new file mode 100644
index 0000000..9188722
--- /dev/null
+++ b/tests/dropship-palletization.test.mjs
@@ -0,0 +1,44 @@
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import { readFile } from 'node:fs/promises'
+
+const read = (path) => readFile(new URL(path, import.meta.url), 'utf8')
+
+test('domestic workbench exposes a real one-click dropship palletization entry', async () => {
+ const [data, home, modulePage, pages] = await Promise.all([
+ read('../data/demo-data.js'), read('../pages/home/home.vue'), read('../pages/module/module.vue'), read('../pages.json')
+ ])
+ assert.match(data, /name: '一件代发合托'[\s\S]*?available: true/)
+ assert.match(home, /'一件代发合托': '\/pages\/dropship-tasks\/dropship-tasks'/)
+ assert.match(modulePage, /'一件代发合托': '\/pages\/dropship-tasks\/dropship-tasks'/)
+ assert.match(pages, /pages\/dropship-tasks\/dropship-tasks/)
+ assert.match(pages, /pages\/dropship-palletization\/dropship-palletization/)
+})
+
+test('dropship palletization adapter uses only the dedicated production PDA contract', async () => {
+ const api = await read('../api/pda-dropship.js')
+ for (const route of [
+ '/tasks/pending', '/cartons/resolve', '/cartons/count', '/pallets/bind-pending',
+ '/pallets/create-and-bind', '/seal'
+ ]) assert.match(api, new RegExp(route.replaceAll('/', '\\/')))
+ assert.match(api, /expectedVersion: pallet\.version/)
+ assert.match(api, /operationId\('count'/)
+ assert.match(api, /operationId\('bind'/)
+ assert.match(api, /operationId\('create-bind'/)
+ assert.match(api, /operationId\('seal'/)
+ assert.doesNotMatch(api, /customerId|tenantId|inboundId/)
+})
+
+test('palletization UI supports scan, count, existing or generated pallet, and seal', async () => {
+ const [tasks, work] = await Promise.all([
+ read('../pages/dropship-tasks/dropship-tasks.vue'),
+ read('../pages/dropship-palletization/dropship-palletization.vue')
+ ])
+ for (const text of ['待合托任务', '入仓码', '进入作业']) assert.match(tasks, new RegExp(text))
+ for (const text of ['领取并开始合托', '扫箱清点', '差异说明', '绑定到扫描托盘', '生成 DP 托盘并绑定', '确认封托']) assert.match(work, new RegExp(text))
+ assert.match(work, /startScanBroadcast/)
+ assert.match(work, /quantityDiffers[\s\S]*?ACCEPT_VARIANCE/)
+ assert.match(work, /canSeal\(\)[\s\S]*?countedCartons[\s\S]*?totalCartons[\s\S]*?unboundCartons/)
+ assert.match(work, /formatApiError/)
+ assert.doesNotMatch(work, /taskId\s*\}\}|warehouseId\s*\}\}|inboundId\s*\}\}/)
+})
diff --git a/tests/page-contract.test.mjs b/tests/page-contract.test.mjs
index ce1441e..3d1d880 100644
--- a/tests/page-contract.test.mjs
+++ b/tests/page-contract.test.mjs
@@ -10,7 +10,8 @@ test('page configuration starts at login and registers independent PDA routes',
assert.equal(pages.pages[0].style.navigationStyle, 'custom')
assert.deepEqual(pages.pages.map((page) => page.path), [
'pages/login/login', 'pages/home/home', 'pages/module/module', 'pages/operation/operation',
- 'pages/tasks/tasks', 'pages/history/history', 'pages/profile/profile', 'pages/scanner-test/scanner-test', 'pages/picking/picking'
+ 'pages/tasks/tasks', 'pages/dropship-tasks/dropship-tasks', 'pages/dropship-palletization/dropship-palletization',
+ 'pages/history/history', 'pages/profile/profile', 'pages/scanner-test/scanner-test', 'pages/picking/picking'
])
})
@@ -87,15 +88,16 @@ test('workbench, module, history and profile are independent sources', async ()
for (const text of ['首页', '库内作业', 'home-operation-grid', 'home-operation-primary', '开始作业', 'uni-icons']) assert.match(home, new RegExp(text))
assert.doesNotMatch(home, /getPickingOverview|pendingBoxCount|箱待拣/)
assert.doesNotMatch(home, /BottomNav|status-strip|task-card/)
- for (const text of ['业务模块', '业务功能', '第一版本仅开放']) assert.match(module, new RegExp(text))
+ for (const text of ['业务模块', '业务功能', '一件代发合托', '集装箱拣货']) assert.match(module, new RegExp(text))
assert.match(home, /:disabled="!operation\.available"/)
assert.match(home, /home-operation-disabled/)
assert.match(home, /home-operation-meta home-operation-pending/)
assert.match(home, /home-operation-meta home-operation-unavailable/)
- assert.match(home, /openOperation\(operation\) \{ if \(operation\.available\) uni\.navigateTo/)
+ assert.match(home, /openOperation\(operation\)[\s\S]*?if \(!operation\.available\) return[\s\S]*?'一件代发合托': '\/pages\/dropship-tasks\/dropship-tasks'[\s\S]*?routes\[operation\.name\]/)
assert.doesNotMatch(home, /pages\/operation\/operation\?module=/)
assert.match(module, /:disabled="!operation\.available"/)
assert.match(module, /operation-item-disabled/)
+ assert.match(module, /openOperation\(operation\)[\s\S]*?if \(!operation\.available\) return[\s\S]*?'一件代发合托': '\/pages\/dropship-tasks\/dropship-tasks'[\s\S]*?routes\[operation\.name\]/)
assert.doesNotMatch(module, /pages\/operation\/operation\?module=/)
assert.match(history, /作业记录/)
assert.match(profile, /个人中心/)