diff --git a/AGENTS.md b/AGENTS.md
index 03c741e..5c58c4c 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,7 +55,7 @@
| 登录与会话 | `pages/login/`、`stores/session.js` | 远程鉴权适配和会话清理 |
| 工作台、仓库选择与模块入口 | `pages/home/`、`components/PortalHeader.vue`、`stores/session.js`、`pages/module/`、`pages/operation/` | 左上角只提供启用国内仓选择,选择结果持久化;`data/demo-data.js` 的 `visible` 控制展示、`available` 控制可操作;隐藏能力不得删除源码或路由 |
| 待处理任务、服务端就绪与领取 | `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 托盘码,PDA 明确执行逐箱清点并支持先扫箱或先扫托、归托和逐托封托;预计托数与已生成托盘数不一致时失败关闭;公网前缀 `/api/platform/v1/first-leg/pda-dropship/**` |
+| 一件代发 | `pages/dropship-tasks/dropship-tasks.vue`、`pages/dropship-palletization/dropship-palletization.vue` | `api/pda-dropship.js`;作为入库作业,测量审核按预计托数预生成 DP 托盘码,PDA 可打开每托一页、含 SKU 明细的 A4 托盘信息单,但不直接驱动打印机;PDA 明确执行逐箱清点并支持先扫箱或先扫托、归托和逐托封托;预计托数与已生成托盘数不一致时失败关闭;公网前缀 `/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` |
diff --git a/README.md b/README.md
index f604236..997b162 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
国内货柜履约装柜取证已具备接口适配基础:拣货页展示空仓、半仓、满仓、锁仓和带锁码照五类证据,经 File 服务上传取得真实 `fileId`,并展示待上传、上传中、失败、成功和重试状态。产品已确认 PDA 只能现场拍照,Admin 可人工上传;每类至少 1 张有效照片、同类型可多张追加,错误或重复照片只作废不硬删除。当前页面和上传队列仍按会话内单类型记录设计,尚未完整达到“多图追加+作废审计”契约。
-登录后默认进入首页,不再直接进入拣货。首页当前只展示“入库 → 一件代发”“出库 → 出库拣货”和“查询 → 库位查询”;其余业务入口保留在数据、页面和路由中,通过 `visible` 隐藏,不从源码删除。首页左上角从 Warehouse 主档选择启用国内仓,首次未选择时自动打开;一件代发与出库拣货待办都按所选仓库从服务端查询。首页不再展示作业记录和个人中心入口,对应页面与路由仍保留。
+登录后默认进入首页,不再直接进入拣货。首页当前只展示“入库 → 一件代发”和“出库 → 出库拣货”;“库位查询”等其余业务入口保留在数据、页面和路由中,通过 `visible` 隐藏,不从源码删除。首页左上角从 Warehouse 主档选择启用国内仓,首次未选择时自动打开;一件代发与出库拣货待办都按所选仓库从服务端查询。首页不再展示作业记录和个人中心入口,对应页面与路由仍保留。
## 页面与状态结构
diff --git a/data/demo-data.js b/data/demo-data.js
index abb6abd..3325658 100644
--- a/data/demo-data.js
+++ b/data/demo-data.js
@@ -25,7 +25,7 @@ export const MODULES = [
] },
{ key: 'query', name: '查询', icon: '查', description: '商品、库位与任务查询', meta: '3 个静态页面', operations: [
{ name: '商品查询', icon: '品', description: '扫描 SKU 查询库存与库位' },
- { name: '库位查询', icon: '位', description: '查看库位状态与在库商品', available: true, visible: true },
+ { name: '库位查询', icon: '位', description: '查看库位状态与在库商品' },
{ name: '任务查询', icon: '单', description: '按单号查询现场任务进度' }
] }
]
diff --git a/pages/dropship-palletization/dropship-palletization.vue b/pages/dropship-palletization/dropship-palletization.vue
index 2c74ec6..9ddb365 100644
--- a/pages/dropship-palletization/dropship-palletization.vue
+++ b/pages/dropship-palletization/dropship-palletization.vue
@@ -29,17 +29,15 @@
- SKU
- {{ carton.skuCode || '—' }}
-
- 清点数量
+
+ SKU {{ index + 1 }}{{ line.skuCode || '—' }}
-
-
-
+
+
+
+ 差异说明
- 差异说明
归入 {{ currentPallet.palletNo }}
@@ -52,13 +50,13 @@
- 2. 托盘码打印贴码,装完后封托
+ 2. 托盘码打开 A4 托盘信息单,装完后封托
暂无托盘
{{ pallet.palletNo }}{{ pallet.status === 'SEALED' ? '已封托' : '待封托' }}
{{ pallet.cartonCount || 0 }} 箱 · {{ pallet.skuCount || 0 }} SKU · {{ pallet.weightKg || 0 }} kg · {{ pallet.volumeCbm || 0 }} m³
-
+
@@ -77,14 +75,13 @@
export default {
components: { PortalHeader, ScanTargetInput },
- data() { return { user: session.currentUser, taskId: '', warehouseId: '', task: null, palletCode: '', selectedPalletId: '', boxCode: '', carton: null, actualQuantity: '', discrepancyRemark: '', activeScan: 'boxCode', loading: false, submitting: false, printing: false, error: '', notice: '' } },
+ data() { return { user: session.currentUser, taskId: '', warehouseId: '', task: null, palletCode: '', selectedPalletId: '', boxCode: '', carton: null, skuCounts: [], activeScan: 'boxCode', loading: false, submitting: false, printing: false, error: '', notice: '' } },
computed: {
statusLabel() { return this.task?.taskStatus === 'COMPLETED' ? '已完成' : (this.task?.taskStatus === 'CLAIMED' ? '作业中' : '待领取') },
palletSetupReady() { return this.task?.palletSetupReady === true },
palletSetupMessage() { return this.task?.palletSetupMessage || `预计 ${this.task?.expectedPalletCount || 0} 托,当前仅生成 ${this.task?.generatedPalletCount || 0} 个托盘码,请联系后台处理后刷新` },
- quantityDiffers() { return this.carton && Number(this.actualQuantity) !== Number(this.carton.expectedSkuQuantity) },
canResolveCarton() { return Boolean(this.palletSetupReady && this.task?.taskStatus === 'CLAIMED' && String(this.boxCode || '').trim() && !this.loading && !this.submitting) },
- canCount() { return Boolean(this.carton?.canCount && Number(this.actualQuantity) > 0 && (!this.quantityDiffers || this.discrepancyRemark.trim())) },
+ canCount() { return Boolean(this.carton?.canCount && this.skuCounts.length && this.skuCounts.every((line) => Number.isInteger(Number(line.actualQuantity)) && Number(line.actualQuantity) >= 0 && (!this.lineDiffers(line) || String(line.discrepancyRemark || '').trim()))) },
currentPallet() { return (this.task?.pallets || []).find((item) => String(item.palletId) === String(this.selectedPalletId) && item.status === 'OPEN') || null }
},
onLoad(options) { this.taskId = String(options.taskId || ''); this.warehouseId = String(options.warehouseId || '') },
@@ -107,15 +104,17 @@
if (!this.palletSetupReady) { this.error = this.palletSetupMessage; return }
const code = String(value || this.boxCode || '').trim().toUpperCase(); if (!code || this.loading) return
this.boxCode = code; this.loading = true; this.resetMessages(); this.carton = null
- try { this.carton = await resolveDropshipCarton(this.taskId, this.warehouseId, code); this.actualQuantity = String(this.carton.expectedSkuQuantity || '') }
+ try { this.carton = await resolveDropshipCarton(this.taskId, this.warehouseId, code); this.skuCounts = (this.carton.skuLines || []).map((line) => ({ ...line, actualQuantity: String(line.expectedQuantity || ''), discrepancyRemark: '' })) }
catch (error) { this.error = formatApiError(error, '箱码解析失败') }
finally { this.loading = false }
},
async countCarton() {
- const result = await this.runMutation(() => countDropshipCarton(this.taskId, { warehouseId: this.warehouseId, boxCode: this.carton.boxCode, palletNo: this.currentPallet?.palletNo || null, actualSkuQuantity: Number(this.actualQuantity), discrepancyAction: this.quantityDiffers ? 'ACCEPT_VARIANCE' : 'MATCH', discrepancyRemark: this.quantityDiffers ? this.discrepancyRemark.trim() : null }), this.currentPallet ? '本箱已清点并归托' : '本箱已完成入库清点')
- if (result) { this.boxCode = ''; this.carton = null; this.actualQuantity = ''; this.discrepancyRemark = '' }
+ const skuLines = this.skuCounts.map((line) => ({ inboundDetailId: line.inboundDetailId, skuCode: line.skuCode, actualQuantity: Number(line.actualQuantity), discrepancyAction: this.lineDiffers(line) ? 'ACCEPT_VARIANCE' : 'MATCH', discrepancyRemark: this.lineDiffers(line) ? String(line.discrepancyRemark || '').trim() : null }))
+ const result = await this.runMutation(() => countDropshipCarton(this.taskId, { warehouseId: this.warehouseId, boxCode: this.carton.boxCode, palletNo: this.currentPallet?.palletNo || null, skuLines }), this.currentPallet ? '本箱已清点并归托' : '本箱已完成入库清点')
+ if (result) { this.boxCode = ''; this.carton = null; this.skuCounts = [] }
},
- adjustQuantity(step) { this.actualQuantity = String(Math.max(1, Number(this.actualQuantity || 0) + step)) },
+ lineDiffers(line) { return Number(line.actualQuantity) !== Number(line.expectedQuantity) },
+ adjustQuantity(index, step) { const line = this.skuCounts[index]; if (line) line.actualQuantity = String(Math.max(0, Number(line.actualQuantity || 0) + step)) },
async selectPallet(value) {
if (!this.palletSetupReady) { this.error = this.palletSetupMessage; return }
const code = String(value || this.palletCode || '').trim().toUpperCase(); const pallet = (this.task?.pallets || []).find((item) => item.palletNo === code)
@@ -137,7 +136,7 @@
// #ifndef H5
await new Promise((resolve, reject) => uni.openDocument({ filePath, fileType: 'pdf', showMenu: true, success: resolve, fail: reject }))
// #endif
- this.notice = `${pallet.palletNo} 打印文件已打开`
+ this.notice = `${pallet.palletNo} A4 托盘信息单已打开`
} catch (error) { this.error = formatApiError(error, '托盘码打印失败') }
finally { this.printing = false }
},
@@ -173,8 +172,8 @@
.section-title { display: block; color: #12233f; font-size: 18px; font-weight: 700; }.section-desc { display: block; margin: 2px 0 8px; color: #667085; font-size: 13px; line-height: 18px; }.section-heading > view { min-width: 0; }
.primary-button, .secondary-button { box-sizing: border-box; width: 100%; height: 46px; margin-top: 10px; border-radius: 9px; font-size: 14px; font-weight: 700; line-height: 44px; }.primary-button { border: 0; color: #fff; background: #1a73e8; }.secondary-button { border: 1px solid #8ab8ef; color: #155eef; background: #f6f9ff; }.primary-button[disabled],.secondary-button[disabled] { opacity: .45; }
.count-entry { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 6px; padding: 5px 7px; border: 0; border-radius: 0; color: #667085; font-size: 13px; background: #f6f9ff; }.compact-action { width: 86px; height: 34px; margin: 0; border: 0; border-radius: 6px; color: #fff; font-size: 13px; line-height: 34px; background: #1a73e8; }.compact-action[disabled] { opacity: .35; }.count-unavailable { padding: 12px 8px; color: #b42318; font-size: 13px; line-height: 18px; text-align: center; background: #fff1f0; }.pallet-scan-block { margin-top: 8px; padding-top: 8px; border-top: 1px solid #e6edf5; }.field-label { display: block; margin-bottom: 5px; color: #344054; font-size: 15px; font-weight: 700; }.field-label text { margin-left: 3px; color: #98a2b3; font-size: 12px; font-weight: 400; }
- .carton-card, .pallet-card { margin-top: 8px; padding: 10px 0 0; border: 0; border-top: 1px solid #e3eaf2; border-radius: 0; background: transparent; }.sku-label { display: block; color: #667085; font-size: 12px; }.sku-code { display: block; margin-top: 3px; color: #12233f; font-size: 19px; font-weight: 700; overflow-wrap: anywhere; }.pallet-meta { display: block; margin-top: 5px; color: #667085; font-size: 14px; line-height: 20px; }
- .quantity-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 14px; color: #344054; font-size: 13px; font-weight: 700; }.quantity-stepper { display: grid; grid-template-columns: 44px 72px 44px; overflow: hidden; border: 1px solid #b8c6d8; border-radius: 9px; background: #fff; }.quantity-stepper button,.quantity-stepper input { box-sizing: border-box; height: 44px; margin: 0; border: 0; border-radius: 0; text-align: center; background: #fff; }.quantity-stepper button { color: #155eef; font-size: 21px; line-height: 44px; }.quantity-stepper input { width: 72px; border-right: 1px solid #dce4ed; border-left: 1px solid #dce4ed; color: #12233f; font-size: 16px; }.pallet-hint { display: block; margin-top: 10px; color: #155eef; font-size: 11px; text-align: center; }
+ .carton-card, .pallet-card { margin-top: 8px; padding: 8px 0 0; border: 0; border-top: 1px solid #e3eaf2; border-radius: 0; background: transparent; }.sku-count-row { display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: center; gap: 8px; padding: 8px 0; border-bottom: 1px solid #edf1f5; }.sku-copy { min-width: 0; }.sku-label { display: block; color: #667085; font-size: 11px; }.sku-code { display: block; margin-top: 2px; color: #12233f; font-size: 16px; font-weight: 700; overflow-wrap: anywhere; }.pallet-meta { display: block; margin-top: 5px; color: #667085; font-size: 14px; line-height: 20px; }
+ .quantity-stepper { display: grid; grid-template-columns: 38px 56px 38px; overflow: hidden; border: 1px solid #b8c6d8; border-radius: 8px; background: #fff; }.quantity-stepper button,.quantity-stepper input { box-sizing: border-box; height: 40px; margin: 0; border: 0; border-radius: 0; text-align: center; background: #fff; }.quantity-stepper button { color: #155eef; font-size: 20px; line-height: 40px; }.quantity-stepper input { width: 56px; border-right: 1px solid #dce4ed; border-left: 1px solid #dce4ed; color: #12233f; font-size: 15px; }.difference-field { grid-column: 1 / -1; margin-top: 0; }.pallet-hint { display: block; margin-top: 10px; color: #155eef; font-size: 11px; text-align: center; }
.field { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 10px; color: #344054; font-size: 12px; }.field input { box-sizing: border-box; width: 120px; height: 42px; padding: 0 10px; border: 1px solid #cfd8e3; border-radius: 8px; background: #fff; }.field.stacked { align-items: stretch; flex-direction: column; gap: 6px; }.field textarea { box-sizing: border-box; width: 100%; height: 70px; padding: 9px; border: 1px solid #cfd8e3; border-radius: 8px; background: #fff; }
.pallet-code { color: #12233f; font-size: 19px; font-weight: 700; overflow-wrap: anywhere; }.text-button { min-width: 58px; height: 40px; border: 0; color: #155eef; font-size: 14px; line-height: 40px; background: transparent; }.empty { padding: 18px 0 8px; color: #98a2b3; font-size: 14px; text-align: center; }
.palletization-page :deep(.scan-target-value) { font-size: 17px; }
diff --git a/pages/module/module.vue b/pages/module/module.vue
index 8c83ddf..7b22309 100644
--- a/pages/module/module.vue
+++ b/pages/module/module.vue
@@ -5,7 +5,7 @@
{{ currentModule.icon }}{{ currentModule.name }}{{ currentModule.description }}
业务功能
- 当前展示“一件代发”“出库拣货”和“库位查询”。
+ 当前展示“一件代发”和“出库拣货”。
diff --git a/tests/dropship-palletization.test.mjs b/tests/dropship-palletization.test.mjs
index 4e7e32c..902a275 100644
--- a/tests/dropship-palletization.test.mjs
+++ b/tests/dropship-palletization.test.mjs
@@ -43,8 +43,10 @@ test('palletization UI accepts carton and pre-generated pallet scans in either o
assert.match(tasks, /\.search-card\s*\{[^}]*display:\s*flex;[^}]*box-shadow:\s*none;/)
assert.match(tasks, /\.task-list\s*\{[^}]*overflow:\s*hidden;[^}]*border-radius:\s*7px;/)
assert.match(tasks, /\.task-card\s*\{[^}]*margin:\s*0;[^}]*border-radius:\s*0;[^}]*box-shadow:\s*none;/)
- for (const text of ['一件代发入库', '领取并开始入库', '扫箱清点', '读取 SKU', '确认清点', '扫托盘', '可选', '差异说明', '托盘码', '打印托盘码', '封托']) assert.match(work, new RegExp(text))
- for (const text of ['sku-label', 'sku-code', 'quantity-stepper', '减少清点数量', '增加清点数量', 'adjustQuantity']) assert.match(work, new RegExp(text))
+ for (const text of ['一件代发入库', '领取并开始入库', '扫箱清点', '读取 SKU', '确认清点', '扫托盘', '可选', '差异说明', '托盘码', '打开 A4 托盘单', '封托']) assert.match(work, new RegExp(text))
+ for (const text of ['sku-label', 'sku-code', 'quantity-stepper', 'adjustQuantity']) assert.match(work, new RegExp(text))
+ assert.match(work, /减少 \$\{line\.skuCode\} 清点数量/)
+ assert.match(work, /增加 \$\{line\.skuCode\} 清点数量/)
for (const text of ['compact-metrics', 'compact-action']) assert.match(work, new RegExp(text))
assert.doesNotMatch(work, /task\.customerName/)
assert.match(work, /\.task-summary-card, \.section-card \{[^}]*border:\s*0;[^}]*border-bottom:\s*6px solid[^}]*border-radius:\s*0;[^}]*box-shadow:\s*none;/)
@@ -59,7 +61,7 @@ test('palletization UI accepts carton and pre-generated pallet scans in either o
assert.match(work, /\.task-summary-card\s*\{\s*display:\s*block;/)
assert.doesNotMatch(work, /class="summary-card"/)
assert.match(work, /startScanBroadcast/)
- assert.match(work, /quantityDiffers[\s\S]*?ACCEPT_VARIANCE/)
+ assert.match(work, /lineDiffers[\s\S]*?ACCEPT_VARIANCE/)
assert.match(work, /bindDropshipPendingCartons/)
assert.match(work, /palletNo: this\.currentPallet/)
assert.match(work, /downloadDropshipPalletLabel/)
diff --git a/tests/page-contract.test.mjs b/tests/page-contract.test.mjs
index 3d63b4b..755cbc3 100644
--- a/tests/page-contract.test.mjs
+++ b/tests/page-contract.test.mjs
@@ -88,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,21 +106,24 @@ test('workbench, module, history and profile are independent sources', async ()
assert.doesNotMatch([home, module, history, profile].join('\n'), /BottomNav/)
})
-test('home exposes only the three confirmed operations and keeps hidden definitions', async () => {
+test('home exposes only the two confirmed operations and keeps hidden definitions', async () => {
const data = await read('../data/demo-data.js')
const { MODULES } = await import('../data/demo-data.js')
for (const text of ['入库', '出库', '库存', '查询', '出库拣货']) assert.match(data, new RegExp(text))
assert.match(data, /key: 'inbound'[\s\S]*?name: '一件代发'[\s\S]*?available: true, visible: true/)
assert.match(data, /key: 'outbound'[\s\S]*?name: '出库拣货'[\s\S]*?available: true, visible: true/)
- assert.match(data, /key: 'query'[\s\S]*?name: '库位查询'[\s\S]*?available: true, visible: true/)
assert.match(await read('../pages/home/home.vue'), /operations: module\.operations\.filter\(\(operation\) => operation\.visible\)/)
assert.deepEqual(MODULES.flatMap((module) => module.operations
.filter((operation) => operation.visible)
.map((operation) => `${module.key}:${operation.name}`)), [
'inbound:一件代发',
- 'outbound:出库拣货',
- 'query:库位查询'
+ 'outbound:出库拣货'
])
+ const locationQuery = MODULES.find((module) => module.key === 'query')?.operations
+ .find((operation) => operation.name === '库位查询')
+ assert.ok(locationQuery)
+ assert.ok(!locationQuery.visible)
+ assert.ok(!locationQuery.available)
assert.ok(MODULES.flatMap((module) => module.operations).some((operation) => operation.name === '到货收货' && !operation.visible))
for (const boxNo of ['CBX-20260709-001', 'CBX-20260709-002', 'CBX-20260709-003']) assert.match(data, new RegExp(boxNo))
})