feat: add dropship palletization workflow

This commit is contained in:
2026-09-08 17:24:52 +08:00
parent 86e49d5edc
commit 0745e9600c
15 changed files with 405 additions and 13 deletions
+49
View File
@@ -0,0 +1,49 @@
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('dropship palletizing route and workbench entry are registered', async () => {
const [pages, data, home, modulePage] = await Promise.all([read('../pages.json'), read('../data/demo-data.js'), read('../pages/home/home.vue'), read('../pages/module/module.vue')])
assert.match(pages, /pages\/dropship-palletizing\/tasks/)
assert.match(pages, /pages\/dropship-palletizing\/dropship-palletizing/)
assert.match(data, /一件代发清点组托[\s\S]*?route: '\/pages\/dropship-palletizing\/tasks'/)
assert.match(home, /operation\.route \|\| '\/pages\/operation\/operation'/)
assert.match(modulePage, /operation\.route \|\| '\/pages\/operation\/operation'/)
})
test('page exposes both scan orders, receiving confirmation, pending boxes, pallet changes, and sealing guards', async () => {
const page = await read('../pages/dropship-palletizing/dropship-palletizing.vue')
for (const text of ['先扫托盘', '先扫多箱', '确认本箱实收', '待绑托箱', '移回待绑托', '封托']) assert.match(page, new RegExp(text))
assert.match(page, /startScanBroadcast/)
assert.match(page, /watchNetwork/)
assert.match(page, /saveDropshipPalletizingDraft/)
assert.match(page, /restoreDropshipPalletizingDraft/)
assert.match(page, /resolveDropshipCarton/)
assert.match(page, /countDropshipCarton/)
assert.match(page, /bindPendingDropshipCartons/)
assert.match(page, /moveDropshipCarton/)
assert.match(page, /sealDropshipPallet/)
assert.match(page, /pallet\.cartons \|\| \[\]/)
assert.match(page, /carton\.canCount === false[\s\S]*?carton\.blockReason/)
assert.doesNotMatch(page, /\(pallet\.items \|\| \[\]\)\.filter/)
assert.match(page, /:disabled="busy \|\| !online \|\| !canSeal"/)
assert.doesNotMatch(page, /warehouseId:\s*Number\(this\.warehouseId\)/)
assert.doesNotMatch(page, /模拟成功|DEMO_|palletStatus/)
})
test('390px work layout uses bounded grids and touch-sized primary actions', async () => {
const page = await read('../pages/dropship-palletizing/dropship-palletizing.vue')
assert.match(page, /\.dropship-page\{min-width:0;overflow:hidden;/)
assert.match(page, /\.summary-grid\{[^}]*grid-template-columns:repeat\(3,minmax\(0,1fr\)\);/)
assert.match(page, /\.primary-button,\.secondary-button\{[^}]*width:100%;min-height:48px;/)
assert.match(page, /\.quantity-control\{[^}]*grid-template-columns:52px minmax\(0,1fr\) 52px/)
})
test('dropship API uses only the frozen cartons/count contract', async () => {
const api = await read('../api/pda-dropship.js')
for (const path of ['tasks/pending', 'cartons/resolve', 'cartons/count', 'pallets/bind-pending', '/move', '/seal']) assert.match(api, new RegExp(path.replace('/', '\\/')))
for (const field of ['actualSkuQuantity', 'discrepancyAction', 'discrepancyRemark', 'palletNo', 'operationId', 'expectedVersion']) assert.match(api, new RegExp(field))
assert.doesNotMatch(api, /pallets\/open|pallets\/\$\{encodeURIComponent\(palletId\)\}\/scan|palletStatus/)
})
+28
View File
@@ -0,0 +1,28 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
const source = await readFile(new URL('../utils/dropship-palletizing-state.js', import.meta.url), 'utf8')
const state = await import(`data:text/javascript;base64,${Buffer.from(source).toString('base64')}`)
test('pallet first selects the pallet and later carton resolution carries it', () => {
let scanState = state.createDropshipScanState()
let plan = state.planDropshipScan(scanState, 'dp-260908-0001')
assert.equal(plan.intent, state.DROPSHIP_SCAN_INTENT.SELECT_PALLET)
scanState = state.releaseDropshipScan(plan.state)
plan = state.planDropshipScan(scanState, 'box-001')
assert.deepEqual({ intent: plan.intent, boxCode: plan.boxCode, palletNo: plan.palletNo }, { intent: state.DROPSHIP_SCAN_INTENT.RESOLVE_CARTON, boxCode: 'BOX-001', palletNo: 'DP-260908-0001' })
})
test('boxes first requests batch binding when a pallet is scanned with pending cartons', () => {
const plan = state.planDropshipScan(state.createDropshipScanState(), 'DP-260908-0002', { unboundCartons: 3 })
assert.equal(plan.intent, state.DROPSHIP_SCAN_INTENT.BIND_PENDING)
assert.equal(plan.palletNo, 'DP-260908-0002')
})
test('offline, empty, and immediate repeated scans are blocked before API intent', () => {
assert.equal(state.planDropshipScan(state.createDropshipScanState({ online: false }), 'BOX-001').intent, state.DROPSHIP_SCAN_INTENT.BLOCKED)
assert.equal(state.planDropshipScan(state.createDropshipScanState(), ' ').intent, state.DROPSHIP_SCAN_INTENT.INVALID)
const first = state.planDropshipScan(state.createDropshipScanState(), 'BOX-001')
assert.equal(state.planDropshipScan(first.state, 'BOX-001').intent, state.DROPSHIP_SCAN_INTENT.DUPLICATE)
})
+6 -5
View File
@@ -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/history/history', 'pages/profile/profile', 'pages/scanner-test/scanner-test', 'pages/picking/picking',
'pages/dropship-palletizing/tasks', 'pages/dropship-palletizing/dropship-palletizing'
])
})
@@ -76,8 +77,8 @@ test('picking task list uses remote loading orders and never falls back to demo
assert.match(tasks, /task\.taskId[\s\S]*?task\.loadingPlanId[\s\S]*?task\.containerBookingId[\s\S]*?task\.currentContainerNo[\s\S]*?task\.version[\s\S]*?task\.assignmentVersion/)
assert.doesNotMatch(tasks, /task\.taskStatus === 'LOADING'/)
assert.doesNotMatch(tasks, /demo-data|DEMO_/)
assert.match(home, /pages\/tasks\/tasks/)
assert.match(modulePage, /pages\/tasks\/tasks/)
assert.match(home, /operation\.route \|\| '\/pages\/operation\/operation'/)
assert.match(modulePage, /operation\.route \|\| '\/pages\/operation\/operation'/)
})
test('workbench, module, history and profile are independent sources', async () => {
@@ -87,7 +88,7 @@ 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/)
@@ -106,7 +107,7 @@ test('workbench, module, history and profile are independent sources', async ()
test('demo model contains all four modules and picking belongs to outbound', async () => {
const data = await read('../data/demo-data.js')
for (const text of ['入库', '出库', '库存', '查询', '集装箱拣货']) assert.match(data, new RegExp(text))
for (const text of ['入库', '出库', '库存', '查询', '集装箱拣货', '一件代发清点组托']) assert.match(data, new RegExp(text))
assert.match(data, /key: 'outbound'[\s\S]*?name: '集装箱拣货'[\s\S]*?available: true/)
for (const boxNo of ['CBX-20260709-001', 'CBX-20260709-002', 'CBX-20260709-003']) assert.match(data, new RegExp(boxNo))
})