Release/test #1

Merged
admin merged 102 commits from release/test into main 2026-09-17 11:53:23 +08:00
18 changed files with 209 additions and 97 deletions
Showing only changes of commit 138ab6405a - Show all commits
+3 -3
View File
@@ -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` 与本文件。
@@ -53,9 +53,9 @@
| 部分拣货布局与库位展示 | `pages/picking/picking.vue`、`styles/pda.scss` | 库位为空时不渲染空标签或残留分隔符;部分拣货数量必须小于需求且不超过服务端库存/库位可用量,合同 `tests/partial-layout.test.mjs`、`tests/picking-location-display.test.mjs` |
| Android APK/WGT 在线更新 | `App.vue`、`utils/app-update.js`、`.env.example`、`manifest.json` | `VITE_PDA_UPDATE_URL` 使用国内独立 HTTPS 通道;合同 `tests/app-update.test.mjs`,H5 不执行更新逻辑 |
| 登录与会话 | `pages/login/`、`stores/session.js` | 远程鉴权适配和会话清理 |
| 工作台与模块入口 | `pages/home/`、`pages/module/`、`pages/operation/` | `data/demo-data.js` 的 `visible` 控制展示、`available` 控制可操作;隐藏能力不得删除源码或路由 |
| 工作台、仓库选择与模块入口 | `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 托盘或系统发新托盘,全部归托后逐托封托;公网前缀 `/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 打印;公网前缀 `/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` |
+1 -1
View File
@@ -8,7 +8,7 @@
国内货柜履约装柜取证已具备接口适配基础:拣货页展示空仓、半仓、满仓、锁仓和带锁码照五类证据,经 File 服务上传取得真实 `fileId`,并展示待上传、上传中、失败、成功和重试状态。产品已确认 PDA 只能现场拍照,Admin 可人工上传;每类至少 1 张有效照片、同类型可多张追加,错误或重复照片只作废不硬删除。当前页面和上传队列仍按会话内单类型记录设计,尚未完整达到“多图追加+作废审计”契约。
登录后默认进入首页,不再直接进入拣货。首页当前只展示“入库 → 一件代发合托”“出库 → 出库拣货”和“查询 → 库位查询”;其余业务入口保留在数据、页面和路由中,通过 `visible` 隐藏,不从源码删除。作业记录收纳在左上角功能菜单,个人中心从右上角人员入口进入。
登录后默认进入首页,不再直接进入拣货。首页当前只展示“入库 → 一件代发”“出库 → 出库拣货”和“查询 → 库位查询”;其余业务入口保留在数据、页面和路由中,通过 `visible` 隐藏,不从源码删除。首页左上角从 Warehouse 主档选择启用国内仓,首次未选择时自动打开;一件代发与出库拣货待办都按所选仓库从服务端查询。首页不再展示作业记录和个人中心入口,对应页面与路由仍保留。
## 页面与状态结构
+30 -8
View File
@@ -1,15 +1,22 @@
import { apiRequest } from '@/api/http.js'
import { apiDownload, apiRequest } from '@/api/http.js'
const ROOT = '/first-leg/pda-dropship'
function stableId(value) {
return String(value ?? '').trim()
}
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 = '') {
export function listDropshipPalletizationTasks(keyword = '', warehouseId) {
if (!warehouseId) throw new Error('请先选择作业仓库')
const query = String(keyword || '').trim()
return apiRequest({ url: `${ROOT}/tasks/pending${query ? `?keyword=${encodeURIComponent(query)}` : ''}`, method: 'GET' })
const params = [`warehouseId=${encodeURIComponent(warehouseId)}`]
if (query) params.push(`keyword=${encodeURIComponent(query)}`)
return apiRequest({ url: `${ROOT}/tasks/pending?${params.join('&')}`, method: 'GET' })
}
export function getDropshipPalletizationTask(taskId, warehouseId) {
@@ -17,7 +24,7 @@ export function getDropshipPalletizationTask(taskId, warehouseId) {
}
export function claimDropshipPalletizationTask(taskId, warehouseId) {
return apiRequest({ url: `${ROOT}/tasks/${encodeURIComponent(taskId)}/claim`, method: 'POST', data: { warehouseId } })
return apiRequest({ url: `${ROOT}/tasks/${encodeURIComponent(taskId)}/claim`, method: 'POST', data: { warehouseId: stableId(warehouseId) } })
}
export function resolveDropshipCarton(taskId, warehouseId, boxCode) {
@@ -25,17 +32,32 @@ export function resolveDropshipCarton(taskId, warehouseId, boxCode) {
}
export function countDropshipCarton(taskId, payload) {
return apiRequest({ url: `${ROOT}/tasks/${encodeURIComponent(taskId)}/cartons/count`, method: 'POST', data: { ...payload, operationId: payload.operationId || operationId('count', taskId) } })
return apiRequest({ url: `${ROOT}/tasks/${encodeURIComponent(taskId)}/cartons/count`, method: 'POST', data: { ...payload, warehouseId: stableId(payload.warehouseId), 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) } })
return apiRequest({ url: `${ROOT}/tasks/${encodeURIComponent(taskId)}/pallets/bind-pending`, method: 'POST', data: { warehouseId: stableId(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) } })
return apiRequest({ url: `${ROOT}/tasks/${encodeURIComponent(taskId)}/pallets/create-and-bind`, method: 'POST', data: { warehouseId: stableId(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) } })
return apiRequest({ url: `${ROOT}/tasks/${encodeURIComponent(taskId)}/pallets/${encodeURIComponent(pallet.palletId)}/seal`, method: 'POST', data: { warehouseId: stableId(warehouseId), expectedVersion: pallet.version, operationId: operationId('seal', taskId) } })
}
export function downloadDropshipPalletLabel(taskId, warehouseId, palletId) {
const url = `${ROOT}/tasks/${encodeURIComponent(taskId)}/pallets/${encodeURIComponent(palletId)}/print?warehouseId=${encodeURIComponent(stableId(warehouseId))}`
return apiDownload({ url }).then((response) => {
const contentType = response.header?.['Content-Type'] || response.header?.['content-type'] || ''
if (response.statusCode < 200 || response.statusCode >= 300 || !response.tempFilePath || (contentType && !/^application\/pdf/i.test(contentType))) {
const error = new Error('托盘码打印文件生成失败')
error.statusCode = response.statusCode
error.requestId = response.header?.['X-Request-Id'] || response.header?.['x-request-id']
error.traceId = response.header?.['X-Trace-Id'] || response.header?.['x-trace-id']
throw error
}
return response.tempFilePath
})
}
+12 -1
View File
@@ -28,6 +28,15 @@ function debugLog(event, payload = {}) {
const request = apiRequest
export function listPdaWarehouses() {
return request({ url: '/first-leg/pda-loading/warehouses', method: 'GET' }).then((records) =>
(Array.isArray(records) ? records : []).map((warehouse) => ({
id: String(warehouse.id),
warehouseCode: String(warehouse.warehouseCode || ''),
warehouseName: String(warehouse.warehouseName || '')
})))
}
function stableIdempotencyKey(operationId, suffix = '') {
const raw = `pda-${operationId}${suffix ? `-${suffix}` : ''}`
let hash = 2166136261
@@ -120,8 +129,10 @@ export async function submitContainerPickingBatch(task, actions, { warehouseId,
})
}
export function listLoadingOrders({ page = 1, size = 50, keyword = '' } = {}) {
export function listLoadingOrders({ page = 1, size = 50, keyword = '', warehouseId } = {}) {
if (!warehouseId) throw new Error('请先选择作业仓库')
const query = []
query.push(`warehouseId=${encodeURIComponent(warehouseId)}`)
if (keyword) query.push(`keyword=${encodeURIComponent(keyword)}`)
const suffix = query.length ? `?${query.join('&')}` : ''
return request({ url: `/first-leg/pda-loading/tasks/pending${suffix}`, method: 'GET' }).then((data) => ({
+12 -6
View File
@@ -2,7 +2,7 @@
<view class="portal-header-shell">
<view class="portal-header">
<view class="portal-header-row">
<button v-if="menu" class="portal-header-button" aria-label="打开功能菜单" @tap="openMenu"><uni-icons type="bars" color="#ffffff" size="26" /></button>
<button v-if="menu" class="portal-header-button" :aria-label="user.warehouse ? `当前作业仓库 ${user.warehouse},点击切换` : '选择作业仓库'" @tap="openMenu"><uni-icons type="location" color="#ffffff" size="26" /></button>
<button v-else class="portal-header-button" aria-label="返回上一页" @tap="goBack"><uni-icons type="back" color="#ffffff" size="26" /></button>
<text class="portal-title">{{ title }}</text>
<button v-if="showProfile" class="avatar-button" aria-label="打开个人中心" @tap="goProfile"><uni-icons type="person" color="#ffffff" size="27" /></button>
@@ -12,9 +12,11 @@
<uni-popup ref="menuPopup" type="bottom" background-color="#ffffff" border-radius="12px 12px 0 0" :is-mask-click="true" @change="handleMenuChange">
<view class="header-menu-sheet">
<view class="header-menu-content">
<PopupHeader title="功能菜单" @close="closeMenu" />
<button class="header-menu-item" @tap="goHistory"><uni-icons type="list" color="#334155" size="22" /><text>作业记录</text><uni-icons type="right" color="#94a3b8" size="18" /></button>
<button class="header-menu-item" @tap="goProfile"><uni-icons type="person" color="#334155" size="22" /><text>个人中心</text><uni-icons type="right" color="#94a3b8" size="18" /></button>
<PopupHeader title="选择作业仓库" @close="closeMenu" />
<view v-if="warehouseLoading" class="header-menu-state">正在加载仓库…</view>
<view v-else-if="warehouseError" class="header-menu-state header-menu-error">{{ warehouseError }}</view>
<view v-else-if="!warehouses.length" class="header-menu-state">暂无可用国内仓</view>
<block v-else><button v-for="warehouse in warehouses" :key="warehouse.id" class="header-menu-item warehouse-menu-item" :class="{ selected: String(warehouse.id) === String(selectedWarehouseId || '') }" @tap="chooseWarehouse(warehouse)"><uni-icons type="location-filled" :color="String(warehouse.id) === String(selectedWarehouseId || '') ? '#155eef' : '#64748b'" size="21" /><view><text>{{ warehouse.warehouseName }}</text><text class="warehouse-menu-code">{{ warehouse.warehouseCode }}</text></view><uni-icons :type="String(warehouse.id) === String(selectedWarehouseId || '') ? 'checkmarkempty' : 'right'" color="#155eef" size="18" /></button></block>
<button class="header-menu-close" @tap="closeMenu">关闭</button>
</view>
</view>
@@ -28,18 +30,22 @@
export default {
components: { PopupHeader },
emits: ['menu-change'],
emits: ['menu-change', 'warehouse-select'],
props: {
title: { type: String, required: true },
user: { type: Object, required: true },
menu: Boolean,
warehouses: { type: Array, default: () => [] },
selectedWarehouseId: { type: [String, Number], default: '' },
warehouseLoading: Boolean,
warehouseError: { type: String, default: '' },
showProfile: { type: Boolean, default: true },
fallbackUrl: { type: String, default: '/pages/home/home' }
},
methods: {
goBack() { navigateBackOr(this.fallbackUrl) },
goProfile() { this.closeMenu(); navigateToUnique('/pages/profile/profile') },
goHistory() { this.closeMenu(); navigateToUnique('/pages/history/history') },
chooseWarehouse(warehouse) { this.$emit('warehouse-select', warehouse); this.closeMenu() },
openMenu() { this.$refs.menuPopup.open() },
closeMenu() { if (this.$refs.menuPopup) this.$refs.menuPopup.close() },
handleMenuChange(event) { this.$emit('menu-change', Boolean(event.show)) }
+1 -1
View File
@@ -8,7 +8,7 @@ export const DEMO_ACCOUNTS = [
export const MODULES = [
{ key: 'inbound', name: '入库', icon: '入', description: '收货、上架与质检', meta: '3 个静态页面', operations: [
{ name: '一件代发合托', icon: '托', description: '逐箱清点并生成或绑定 DP 托盘', available: true, visible: true },
{ name: '一件代发', icon: '托', description: '完成逐箱入库后生成并打印 DP 托盘码', available: true, visible: true },
{ name: '到货收货', icon: '收', description: '扫描到货单并确认实收数量' },
{ name: '商品上架', icon: '上', description: '按推荐库位完成商品上架' },
{ name: '入库质检', icon: '检', description: '登记质检结果与异常信息' }
+1 -1
View File
@@ -5,7 +5,7 @@
{ "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-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" } },
@@ -1,12 +1,12 @@
<template>
<view v-if="user" class="page-shell palletization-page">
<PortalHeader title="合托作业" :user="user" :fallback-url="'/pages/dropship-tasks/dropship-tasks'" />
<PortalHeader title="一件代发入库" :user="user" :fallback-url="'/pages/dropship-tasks/dropship-tasks'" />
<view class="content">
<view v-if="error" class="message error" role="alert">{{ error }}</view>
<view v-if="notice" class="message success" role="status">{{ notice }}</view>
<view v-if="loading && !task" class="state">正在加载合托任务…</view>
<view v-if="loading && !task" class="state">正在加载入库任务…</view>
<template v-if="task">
<view class="summary-card">
<view class="task-summary-card">
<view class="summary-head"><view><text class="eyebrow">入仓码</text><text class="inbound-code">{{ task.inboundCode || '—' }}</text></view><text class="status">{{ statusLabel }}</text></view>
<text class="customer">{{ task.customerName || '客户未关联' }} · {{ task.warehouseName || '仓库未关联' }}</text>
<view class="metrics">
@@ -15,7 +15,7 @@
<view><strong>{{ task.boundCartons || 0 }}</strong><text>已归托</text></view>
<view><strong>{{ task.unboundCartons || 0 }}</strong><text>待归托</text></view>
</view>
<button v-if="task.taskStatus === 'PENDING'" class="primary-button" :disabled="submitting" @tap="claimTask">领取并开始合托</button>
<button v-if="task.taskStatus === 'PENDING'" class="primary-button" :disabled="submitting" @tap="claimTask">领取并开始入库</button>
</view>
<view v-if="task.taskStatus === 'CLAIMED'" class="section-card">
@@ -33,20 +33,18 @@
</view>
<view v-if="task.taskStatus === 'CLAIMED'" class="section-card">
<text class="section-title">2. 归托</text>
<text class="section-desc">把已清点待归托箱绑定到现有 DP 托盘,或由系统生成新托盘。</text>
<ScanTargetInput v-model="palletNo" name="palletNo" label="托盘码" placeholder="扫描 DP-YYMMDD-NNNN" :active="activeScan === 'palletNo'" @activate="activeScan = 'palletNo'" @submit="bindPending" />
<button class="secondary-button" :disabled="!validPalletNo || !task.unboundCartons || submitting" @tap="bindPending">绑定到扫描托盘</button>
<button class="primary-button" :disabled="!task.unboundCartons || submitting" @tap="createAndBind">生成 DP 托盘并绑定</button>
<text class="section-title">2. 完成入库</text>
<text class="section-desc">全部箱码清点完成后,系统一次性完成归托、封托并生成正式 DP 托盘码。</text>
<button class="primary-button" :disabled="!allCartonsCounted || !task.unboundCartons || submitting" @tap="completeInbound">完成入库并生成托盘码</button>
</view>
<view class="section-card">
<view class="section-heading"><view><text class="section-title">3. 托盘与封托</text><text class="section-desc">所有箱清点并归托后,逐托确认封托。</text></view><button class="text-button" :disabled="loading" @tap="loadTask">刷新</button></view>
<view class="section-heading"><view><text class="section-title">3. 托盘码与打印</text><text class="section-desc">完成入库后显示最终托盘码,可立即打印并粘贴。</text></view><button class="text-button" :disabled="loading" @tap="loadTask">刷新</button></view>
<view v-if="!task.pallets || !task.pallets.length" class="empty">暂无托盘</view>
<view v-for="pallet in task.pallets" :key="pallet.palletId" class="pallet-card">
<view class="pallet-head"><text class="pallet-code">{{ pallet.palletNo }}</text><text class="pallet-status" :class="pallet.status === 'SEALED' ? 'sealed' : ''">{{ pallet.status === 'SEALED' ? '已封托' : '待封托' }}</text></view>
<text class="pallet-meta">{{ pallet.cartonCount || 0 }} 箱 · {{ pallet.skuCount || 0 }} SKU · {{ pallet.weightKg || 0 }} kg · {{ pallet.volumeCbm || 0 }} m³</text>
<button v-if="pallet.status === 'OPEN'" class="primary-button" :disabled="submitting || !canSeal" @tap="sealPallet(pallet)">确认封托</button>
<button v-if="pallet.status === 'SEALED'" class="primary-button" :disabled="printing" @tap="printPallet(pallet)">{{ printing ? '正在生成打印文件…' : '打印托盘码' }}</button>
</view>
</view>
</template>
@@ -57,20 +55,19 @@
<script>
import PortalHeader from '@/components/PortalHeader.vue'
import ScanTargetInput from '@/components/ScanTargetInput.vue'
import { bindDropshipPendingCartons, claimDropshipPalletizationTask, countDropshipCarton, createAndBindDropshipPallet, getDropshipPalletizationTask, resolveDropshipCarton, sealDropshipPallet } from '@/api/pda-dropship.js'
import { claimDropshipPalletizationTask, countDropshipCarton, createAndBindDropshipPallet, downloadDropshipPalletLabel, getDropshipPalletizationTask, resolveDropshipCarton } from '@/api/pda-dropship.js'
import { session, requireSession } from '@/stores/session.js'
import { formatApiError } from '@/utils/user-facing.js'
import { startScanBroadcast } from '@/utils/scan-broadcast.js'
export default {
components: { PortalHeader, ScanTargetInput },
data() { return { user: session.currentUser, taskId: '', warehouseId: '', task: null, boxCode: '', palletNo: '', carton: null, actualQuantity: '', discrepancyRemark: '', activeScan: 'boxCode', loading: false, submitting: false, error: '', notice: '' } },
data() { return { user: session.currentUser, taskId: '', warehouseId: '', task: null, boxCode: '', carton: null, actualQuantity: '', discrepancyRemark: '', activeScan: 'boxCode', loading: false, submitting: false, printing: false, error: '', notice: '' } },
computed: {
statusLabel() { return this.task?.taskStatus === 'COMPLETED' ? '已完成' : (this.task?.taskStatus === 'CLAIMED' ? '作业中' : '待领取') },
quantityDiffers() { return this.carton && Number(this.actualQuantity) !== Number(this.carton.expectedSkuQuantity) },
canCount() { return Boolean(this.carton?.canCount && Number(this.actualQuantity) > 0 && (!this.quantityDiffers || this.discrepancyRemark.trim())) },
validPalletNo() { return /^DP-\d{6}-\d{4}$/.test(this.palletNo.trim().toUpperCase()) },
canSeal() { return Number(this.task?.countedCartons || 0) === Number(this.task?.totalCartons || 0) && Number(this.task?.unboundCartons || 0) === 0 }
allCartonsCounted() { return Number(this.task?.totalCartons || 0) > 0 && Number(this.task?.countedCartons || 0) === Number(this.task?.totalCartons || 0) }
},
onLoad(options) { this.taskId = String(options.taskId || ''); this.warehouseId = String(options.warehouseId || '') },
onShow() { if (!requireSession()) return; this.user = session.currentUser; this.startScanner(); this.loadTask() },
@@ -78,16 +75,16 @@
methods: {
startScanner() { this.stopScanner(); this._stopScanner = startScanBroadcast({ onScan: (value) => this.handleScan(value), onError: () => { this.error = '扫码头连接失败,可使用输入框继续作业' } }) },
stopScanner() { if (this._stopScanner) this._stopScanner(); this._stopScanner = null },
handleScan(value) { if (this.activeScan === 'palletNo') { this.palletNo = String(value).trim().toUpperCase(); this.bindPending() } else { this.boxCode = String(value).trim().toUpperCase(); this.resolveCarton() } },
handleScan(value) { this.boxCode = String(value).trim().toUpperCase(); this.resolveCarton() },
resetMessages() { this.error = ''; this.notice = '' },
async loadTask() {
if (!this.taskId || !this.warehouseId) { this.error = '缺少合托任务或仓库信息,请返回任务列表'; return }
this.loading = true; this.resetMessages()
try { this.task = await getDropshipPalletizationTask(this.taskId, this.warehouseId) }
catch (error) { this.error = formatApiError(error, '合托任务加载失败') }
catch (error) { this.error = formatApiError(error, '一件代发入库任务加载失败') }
finally { this.loading = false }
},
async claimTask() { await this.runMutation(() => claimDropshipPalletizationTask(this.taskId, Number(this.warehouseId)), '任务领取成功') },
async claimTask() { await this.runMutation(() => claimDropshipPalletizationTask(this.taskId, this.warehouseId), '任务领取成功') },
async resolveCarton(value) {
const code = String(value || this.boxCode || '').trim().toUpperCase(); if (!code || this.loading) return
this.boxCode = code; this.loading = true; this.resetMessages(); this.carton = null
@@ -96,12 +93,25 @@
finally { this.loading = false }
},
async countCarton() {
const result = await this.runMutation(() => countDropshipCarton(this.taskId, { warehouseId: Number(this.warehouseId), boxCode: this.carton.boxCode, actualSkuQuantity: Number(this.actualQuantity), discrepancyAction: this.quantityDiffers ? 'ACCEPT_VARIANCE' : 'MATCH', discrepancyRemark: this.quantityDiffers ? this.discrepancyRemark.trim() : null, palletNo: null }), '本箱已清点,等待归托')
const result = await this.runMutation(() => countDropshipCarton(this.taskId, { warehouseId: this.warehouseId, boxCode: this.carton.boxCode, actualSkuQuantity: Number(this.actualQuantity), discrepancyAction: this.quantityDiffers ? 'ACCEPT_VARIANCE' : 'MATCH', discrepancyRemark: this.quantityDiffers ? this.discrepancyRemark.trim() : null }), '本箱已完成入库清点')
if (result) { this.boxCode = ''; this.carton = null; this.actualQuantity = ''; this.discrepancyRemark = '' }
},
async bindPending(value) { const code = String(value || this.palletNo || '').trim().toUpperCase(); if (!/^DP-\d{6}-\d{4}$/.test(code)) { this.error = '请输入正确的 DP 托盘码'; return }; this.palletNo = code; await this.runMutation(() => bindDropshipPendingCartons(this.taskId, Number(this.warehouseId), code), '待归托箱已绑定') },
async createAndBind() { await this.runMutation(() => createAndBindDropshipPallet(this.taskId, Number(this.warehouseId)), '已生成 DP 托盘并绑定') },
async sealPallet(pallet) { await this.runMutation(() => sealDropshipPallet(this.taskId, Number(this.warehouseId), pallet), `${pallet.palletNo} 已封托`) },
async completeInbound() { await this.runMutation(() => createAndBindDropshipPallet(this.taskId, this.warehouseId), '入库已完成,正式托盘码已生成') },
async printPallet(pallet) {
if (this.printing || pallet.status !== 'SEALED') return
this.printing = true; this.resetMessages()
try {
const filePath = await downloadDropshipPalletLabel(this.taskId, this.warehouseId, pallet.palletId)
// #ifdef H5
const link = document.createElement('a'); link.href = filePath; link.download = `${pallet.palletNo}-托盘码.pdf`; link.click()
// #endif
// #ifndef H5
await new Promise((resolve, reject) => uni.openDocument({ filePath, fileType: 'pdf', showMenu: true, success: resolve, fail: reject }))
// #endif
this.notice = `${pallet.palletNo} 打印文件已打开`
} catch (error) { this.error = formatApiError(error, '托盘码打印失败') }
finally { this.printing = false }
},
async runMutation(action, message) {
if (this.submitting) return null
this.submitting = true; this.resetMessages()
@@ -124,7 +134,8 @@
.content { padding: 12px 12px 42px; }
.state, .message { margin-bottom: 10px; padding: 12px; border-radius: 9px; color: #667085; font-size: 12px; line-height: 18px; text-align: center; background: #fff; }
.message.error { color: #b42318; background: #fff1f0; }.message.success { color: #067647; background: #ecfdf3; }
.summary-card, .section-card { margin-bottom: 12px; padding: 13px; border: 1px solid #dce6f1; border-radius: 12px; background: #fff; box-shadow: 0 2px 8px rgba(23,55,93,.05); }
.task-summary-card, .section-card { margin-bottom: 12px; padding: 13px; border: 1px solid #dce6f1; border-radius: 12px; background: #fff; box-shadow: 0 2px 8px rgba(23,55,93,.05); }
.task-summary-card { display: block; }
.summary-head, .section-heading, .pallet-head, .carton-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.summary-head > view { display: flex; min-width: 0; flex-direction: column; }.eyebrow { color: #667085; font-size: 10px; }.inbound-code { margin-top: 2px; color: #12233f; font-size: 19px; font-weight: 700; overflow-wrap: anywhere; }
.status, .pallet-status { flex-shrink: 0; padding: 4px 8px; border-radius: 99px; color: #155eef; font-size: 10px; background: #eff4ff; }.pallet-status.sealed { color: #067647; background: #ecfdf3; }
+33 -24
View File
@@ -1,19 +1,19 @@
<template>
<view v-if="user" class="page-shell dropship-task-page">
<PortalHeader title="一件代发合托" :user="user" :fallback-url="'/pages/home/home'" />
<PortalHeader title="一件代发" :user="user" :fallback-url="'/pages/home/home'" />
<view class="content">
<view class="search-card">
<ScanTargetInput v-model="keyword" name="taskKeyword" label="入仓码查询" placeholder="扫描或输入入仓码" :active="true" @submit="loadTasks" />
<button class="primary-button search-button" :disabled="loading" @tap="loadTasks">{{ loading ? '查询中…' : '查询任务' }}</button>
</view>
<view class="heading-row">
<view><text class="heading">待合托任务</text><text class="summary">{{ loading ? '正在同步' : `共 ${tasks.length} 个任务` }}</text></view>
<view><text class="heading">待入库任务</text><text class="summary">{{ loading ? '正在同步' : `共 ${tasks.length} 个任务` }}</text></view>
<button class="refresh-button" :disabled="loading" @tap="refresh">刷新</button>
</view>
<view v-if="error" class="state error" role="alert">{{ error }}</view>
<view v-else-if="!loading && !tasks.length" class="state">暂无待合托任务</view>
<view v-else-if="!loading && !tasks.length" class="state">暂无待入库任务</view>
<view class="task-list">
<button v-for="task in tasks" :key="task.taskId" class="task-card" :aria-label="`进入合托任务,入仓码 ${task.inboundCode || '未关联'}`" @tap="openTask(task)">
<button v-for="task in tasks" :key="task.taskId" class="task-card" :aria-label="`进入一件代发入库任务,入仓码 ${task.inboundCode || '未关联'}`" @tap="openTask(task)">
<view class="task-head"><text class="business-code">{{ task.inboundCode || '入仓码 —' }}</text><text class="status" :class="task.taskStatus === 'CLAIMED' ? 'claimed' : ''">{{ statusLabel(task.taskStatus) }}</text></view>
<text class="customer">{{ task.customerName || '客户未关联' }}</text>
<view class="metrics">
@@ -45,12 +45,13 @@
refresh() { this.keyword = ''; this.loadTasks() },
async loadTasks() {
if (this.loading) return
if (!this.user?.warehouseId) { this.error = '请返回首页选择作业仓库'; return }
this.loading = true; this.error = ''
try {
const records = await listDropshipPalletizationTasks(this.keyword)
const records = await listDropshipPalletizationTasks(this.keyword, this.user.warehouseId)
this.tasks = Array.isArray(records) ? records : []
}
catch (error) { this.tasks = []; this.error = formatApiError(error, '合托任务加载失败') }
catch (error) { this.tasks = []; this.error = formatApiError(error, '一件代发入库任务加载失败') }
finally { this.loading = false }
},
openTask(task) { uni.navigateTo({ url: `/pages/dropship-palletization/dropship-palletization?taskId=${encodeURIComponent(task.taskId)}&warehouseId=${encodeURIComponent(task.warehouseId)}` }) }
@@ -59,28 +60,36 @@
</script>
<style scoped>
.content { padding: 12px 12px 40px; }
.search-card { padding: 12px; border: 1px solid #dce6f1; border-radius: 12px; background: #fff; box-shadow: 0 2px 8px rgba(23,55,93,.05); }
.primary-button { height: 46px; border: 0; border-radius: 9px; color: #fff; font-size: 15px; font-weight: 700; line-height: 46px; background: #1a73e8; }
.search-button { width: 100%; margin-top: 10px; }
.heading-row { display: flex; align-items: center; justify-content: space-between; margin: 16px 0 10px; }
.heading-row > view { display: flex; flex-direction: column; }
.heading { color: #12233f; font-size: 18px; font-weight: 700; }
.summary { margin-top: 2px; color: #667085; font-size: 11px; }
.refresh-button { min-width: 72px; height: 42px; border: 0; border-radius: 9px; color: #155eef; font-size: 14px; line-height: 42px; background: #eaf2ff; }
.content { padding: 8px 10px 32px; }
.search-card { display: flex; align-items: center; gap: 8px; padding: 0; border: 0; background: transparent; box-shadow: none; }
.search-card :deep(.scan-target-input) { min-width: 0; height: 44px; flex: 1; border-radius: 6px; }
.search-card :deep(.scan-target-value) { height: 42px; }
.search-card :deep(.scan-target-icon) { top: 10px; }
.search-card :deep(.scan-target-input-active .scan-target-icon) { top: 9px; }
.search-card :deep(.scan-target-badge) { top: 10px; }
.primary-button { height: 44px; border: 0; border-radius: 6px; color: #fff; font-size: 14px; font-weight: 700; line-height: 44px; background: #1a73e8; }
.search-button { width: 92px; margin: 0; flex-shrink: 0; }
.heading-row { display: flex; align-items: center; justify-content: space-between; margin: 10px 0 7px; }
.heading-row > view { display: flex; align-items: baseline; gap: 8px; }
.heading { color: #12233f; font-size: 17px; font-weight: 700; }
.summary { color: #667085; font-size: 11px; }
.refresh-button { min-width: 64px; height: 40px; border: 0; border-radius: 6px; color: #155eef; font-size: 13px; line-height: 40px; background: #eaf2ff; }
.state { padding: 24px 12px; border-radius: 10px; color: #667085; font-size: 13px; text-align: center; background: #fff; }
.state.error { color: #b42318; background: #fff1f0; }
.task-card { box-sizing: border-box; width: 100%; margin: 0 0 12px; padding: 13px; border: 1px solid #dce6f1; border-radius: 12px; color: #12233f; text-align: left; background: #fff; box-shadow: 0 2px 8px rgba(23,55,93,.05); }
.task-list { overflow: hidden; border: 1px solid #dce6f1; border-radius: 7px; background: #fff; }
.task-card { box-sizing: border-box; width: 100%; margin: 0; padding: 9px 11px 7px; border: 0; border-bottom: 1px solid #dce6f1; border-radius: 0; color: #12233f; text-align: left; background: #fff; box-shadow: none; }
.task-card:last-child { border-bottom: 0; }
.task-card:active { background: #f5f8fc; }
.task-head, .task-foot { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.business-code { min-width: 0; font-size: 17px; font-weight: 700; overflow-wrap: anywhere; }
.status { flex-shrink: 0; padding: 4px 8px; border-radius: 99px; color: #9a5b13; font-size: 10px; background: #fff4e5; }
.business-code { min-width: 0; font-size: 16px; font-weight: 700; line-height: 22px; overflow-wrap: anywhere; }
.status { flex-shrink: 0; padding: 2px 7px; border-radius: 4px; color: #9a5b13; font-size: 10px; line-height: 18px; background: #fff4e5; }
.status.claimed { color: #155eef; background: #eff4ff; }
.customer { display: block; margin-top: 5px; color: #667085; font-size: 11px; }
.metrics { display: grid; grid-template-columns: repeat(4, 1fr); margin-top: 12px; padding: 10px 0; border-top: 1px solid #edf1f5; border-bottom: 1px solid #edf1f5; }
.customer { display: block; margin-top: 1px; color: #667085; font-size: 11px; line-height: 17px; }
.metrics { display: grid; grid-template-columns: repeat(4, 1fr); margin-top: 6px; padding: 6px 0; border-top: 1px solid #edf1f5; border-bottom: 1px solid #edf1f5; }
.metrics view { display: flex; align-items: center; flex-direction: column; border-right: 1px solid #edf1f5; }
.metrics view:last-child { border-right: 0; }
.metrics strong { font-size: 17px; }
.metrics text { margin-top: 2px; color: #667085; font-size: 10px; }
.task-foot { margin-top: 10px; color: #667085; font-size: 11px; }
.enter { min-height: 40px; color: #155eef; font-size: 12px; font-weight: 700; line-height: 40px; }
.metrics strong { font-size: 16px; line-height: 22px; }
.metrics text { color: #667085; font-size: 10px; line-height: 16px; }
.task-foot { margin-top: 3px; color: #667085; font-size: 11px; line-height: 30px; }
.enter { color: #155eef; font-size: 12px; font-weight: 700; }
</style>
+19 -6
View File
@@ -1,6 +1,6 @@
<template>
<view v-if="user" class="page-shell portal-page home-page">
<PortalHeader ref="portalHeader" title="首页" :user="user" menu @menu-change="menuOpen = $event" />
<PortalHeader ref="portalHeader" :title="user.warehouse ? `首页 · ${user.warehouse}` : '首页'" :user="user" menu :show-profile="false" :warehouses="warehouses" :selected-warehouse-id="user.warehouseId" :warehouse-loading="warehouseLoading" :warehouse-error="warehouseError" @warehouse-select="changeWarehouse" @menu-change="menuOpen = $event" />
<view class="home-menu">
<view v-for="group in groups" :key="group.key" class="home-group" :class="{ 'home-group-disabled': !group.available }">
<view class="home-group-title"><view class="home-group-mark" :class="`home-group-mark-${group.key}`"></view><text>{{ group.name }}</text></view>
@@ -20,11 +20,12 @@
<script>
import PortalHeader from '@/components/PortalHeader.vue'
import { MODULES } from '@/data/demo-data.js'
import { session, requireSession } from '@/stores/session.js'
import { listPdaWarehouses } from '@/api/pda-loading.js'
import { session, requireSession, selectWarehouse } from '@/stores/session.js'
const ICON_TYPES = {
'到货收货': 'download', '商品上架': 'upload', '入库质检': 'checkbox',
'一件代发合托': 'map', '出库拣货': 'cart', '出库复核': 'compose', '装箱交接': 'paperplane',
'一件代发': 'map', '出库拣货': 'cart', '出库复核': 'compose', '装箱交接': 'paperplane',
'库存盘点': 'list', '商品移库': 'loop', '库存调整': 'compose',
'商品查询': 'search', '库位查询': 'location', '任务查询': 'flag'
}
@@ -39,18 +40,30 @@
export default {
components: { PortalHeader },
data() { return { user: session.currentUser, groups: HOME_GROUPS, menuOpen: false } },
onShow() { if (!requireSession()) return; this.user = session.currentUser },
data() { return { user: session.currentUser, groups: HOME_GROUPS, menuOpen: false, warehouses: [], warehouseLoading: false, warehouseError: '' } },
onShow() { if (!requireSession()) return; this.user = session.currentUser; this.loadWarehouses() },
onBackPress() {
if (!this.menuOpen) return false
this.$refs.portalHeader.closeMenu()
return true
},
methods: {
async loadWarehouses() {
if (this.warehouseLoading) return
this.warehouseLoading = true; this.warehouseError = ''
try {
this.warehouses = await listPdaWarehouses()
if (!this.user.warehouseId) this.$nextTick(() => this.$refs.portalHeader.openMenu())
} catch (error) {
this.warehouseError = error.message || '仓库加载失败,请稍后重试'
} finally { this.warehouseLoading = false }
},
changeWarehouse(warehouse) { this.user = selectWarehouse(warehouse) },
openOperation(operation) {
if (!operation.available) return
if (!this.user.warehouseId) { this.$refs.portalHeader.openMenu(); return }
const routes = {
'一件代发合托': '/pages/dropship-tasks/dropship-tasks',
'一件代发': '/pages/dropship-tasks/dropship-tasks',
'出库拣货': '/pages/tasks/tasks',
'库位查询': `/pages/operation/operation?module=query&name=${encodeURIComponent('库位查询')}`
}
+2 -2
View File
@@ -5,7 +5,7 @@
<view class="module-hero" :class="`module-hero-${currentModule.key}`"><view class="module-hero-icon">{{ currentModule.icon }}</view><view><text class="module-hero-title">{{ currentModule.name }}</text><text class="module-hero-desc">{{ currentModule.description }}</text></view></view>
<text class="group-title">业务功能</text>
<view class="operation-list"><button v-for="operation in currentModule.operations" :key="operation.name" class="operation-item" :class="{ 'operation-item-disabled': !operation.available }" :disabled="!operation.available" @tap="openOperation(operation)"><view class="operation-icon">{{ operation.icon }}</view><view class="operation-info"><text class="operation-name">{{ operation.name }}</text><text class="operation-desc">{{ operation.description }}</text></view><text v-if="operation.available" class="operation-count">{{ overview.pendingBoxCount }}</text><text v-else class="operation-prototype">未开放</text><text v-if="operation.available" class="operation-arrow">›</text></button></view>
<view class="module-empty-note">当前展示“一件代发合托”“出库拣货”和“库位查询”。</view>
<view class="module-empty-note">当前展示“一件代发”“出库拣货”和“库位查询”。</view>
</view>
</view>
</template>
@@ -29,7 +29,7 @@
methods: {
openOperation(operation) {
if (!operation.available) return
const routes = { '一件代发合托': '/pages/dropship-tasks/dropship-tasks', '出库拣货': '/pages/tasks/tasks' }
const routes = { '一件代发': '/pages/dropship-tasks/dropship-tasks', '出库拣货': '/pages/tasks/tasks' }
const url = routes[operation.name]
|| `/pages/operation/operation?module=${encodeURIComponent(this.moduleKey)}&name=${encodeURIComponent(operation.name)}`
uni.navigateTo({ url })
+1 -1
View File
@@ -598,7 +598,7 @@
async refreshAvailableBoxes() {
if (!session.token) return
try {
const result = await listLoadingOrders({ page: 1, size: 50 })
const result = await listLoadingOrders({ page: 1, size: 50, warehouseId: session.currentUser?.warehouseId })
const records = (result.records || []).filter((task) => this.isSelectableTask(task))
const previousBoxes = [...this.boxes]
const selectedTaskId = String(this.remoteTaskId || this.currentBox.taskId || '')
+2 -1
View File
@@ -67,9 +67,10 @@
isTaskVisible(task) { return Boolean(task.recordStatus === 'ACTIVE' && !task.mergedIntoContainerBookingId && task.taskId && ['UNASSIGNED', 'IN_PROGRESS', 'AWAITING_REVIEW', 'PROCESSING', 'COMPLETION_FAILED'].includes(task.workflowStatus)) },
async loadTasks() {
if (!isOnline()) { this.error = '当前网络不可用,请联网后刷新'; return }
if (!this.user?.warehouseId) { this.error = '请返回首页选择作业仓库'; return }
this.loading = true; this.error = ''; this.notice = ''
try {
const result = await listLoadingOrders({ page: 1, size: 50 })
const result = await listLoadingOrders({ page: 1, size: 50, warehouseId: this.user.warehouseId })
const records = result.records || []
records.filter((task) => this.isMergedTask(task)).forEach((task) => this.clearMergedTaskDraft(task))
this.tasks = records.filter((task) => this.isTaskVisible(task))
+15
View File
@@ -93,6 +93,21 @@ export function signInWithTokens(tokens) {
})
}
export function selectWarehouse(warehouse) {
if (!session.currentUser) throw new Error('请先登录')
if (!warehouse?.id || !warehouse?.warehouseName) throw new Error('请选择有效的作业仓库')
const warehouseId = String(warehouse.id)
if (String(session.currentUser.warehouseId || '') !== warehouseId) resetPickingSession()
session.currentUser = {
...session.currentUser,
warehouseId,
warehouse: String(warehouse.warehouseName),
warehouseCode: String(warehouse.warehouseCode || '')
}
persistSession()
return session.currentUser
}
export function updateTokenPair(tokens) {
if (!tokens?.accessToken) throw new Error('刷新响应缺少 accessToken')
if (!tokens?.refreshToken) throw new Error('刷新响应缺少 refreshToken')
+6
View File
@@ -795,6 +795,12 @@
.header-menu-content { padding-top: 1px; }
.header-menu-title { display: block; margin: 17px 16px 8px; color: #1f2937; font-size: 17px; font-weight: 700; }
.header-menu-item { display: grid; box-sizing: border-box; width: calc(100% - 32px); height: 54px; margin: 0 16px; padding: 0 4px; align-items: center; grid-template-columns: 32px minmax(0, 1fr) 24px; border-bottom: 1px solid #e5e7eb; color: #334155; font-size: 15px; line-height: 54px; text-align: left; background: #ffffff; }
.header-menu-state { padding: 26px 16px; color: #64748b; font-size: 13px; text-align: center; }
.header-menu-error { color: #b42318; }
.warehouse-menu-item { height: 58px; line-height: normal; }
.warehouse-menu-item.selected { color: #155eef; background: #f5f8ff; }
.warehouse-menu-item > view { display: flex; min-width: 0; flex-direction: column; gap: 3px; }
.warehouse-menu-code { color: #8491a3; font-size: 10px; }
.header-menu-close { box-sizing: border-box; width: calc(100% - 32px); height: 48px; margin: 12px 16px calc(14px + env(safe-area-inset-bottom, 0px)); border-radius: 6px; color: #334155; font-size: 14px; line-height: 48px; background: #eef2f6; }
.group-title { font-size: 16px; font-weight: 700; }
+26 -12
View File
@@ -8,9 +8,9 @@ test('domestic workbench exposes a real one-click dropship palletization entry',
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, /key: 'inbound'[\s\S]*?name: '一件代发合托'[\s\S]*?available: true, visible: true/)
assert.match(home, /'一件代发合托': '\/pages\/dropship-tasks\/dropship-tasks'/)
assert.match(modulePage, /'一件代发合托': '\/pages\/dropship-tasks\/dropship-tasks'/)
assert.match(data, /key: 'inbound'[\s\S]*?name: '一件代发'[\s\S]*?available: true, visible: 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/)
})
@@ -18,27 +18,41 @@ test('domestic workbench exposes a real one-click dropship palletization entry',
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'
'/tasks/pending', '/cartons/resolve', '/cartons/count',
'/pallets/create-and-bind', '/print'
]) assert.match(api, new RegExp(route.replaceAll('/', '\\/')))
assert.match(api, /expectedVersion: pallet\.version/)
assert.match(api, /apiDownload/)
assert.match(api, /application\\\/pdf/)
assert.match(api, /tasks\/pending\?\$\{params\.join\('&'\)\}/)
assert.match(api, /warehouseId=\$\{encodeURIComponent\(warehouseId\)\}/)
assert.match(api, /operationId\('count'/)
assert.match(api, /operationId\('bind'/)
assert.match(api, /operationId\('create-bind'/)
assert.match(api, /operationId\('seal'/)
assert.match(api, /function stableId\(value\)[\s\S]*?String\(value \?\? ''\)\.trim\(\)/)
assert.match(api, /warehouseId: stableId\(warehouseId\)/)
assert.match(api, /warehouseId: stableId\(payload\.warehouseId\)/)
assert.doesNotMatch(api, /customerId|tenantId|inboundId/)
})
test('palletization UI supports scan, count, existing or generated pallet, and seal', async () => {
test('palletization UI generates and prints the final pallet number only after inbound work is complete', 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))
for (const text of ['待入库任务', '入仓码', '进入作业']) assert.match(tasks, new RegExp(text))
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 ['一件代发入库', '领取并开始入库', '扫箱清点', '差异说明', '完成入库并生成托盘码', '托盘码与打印', '打印托盘码']) assert.match(work, new RegExp(text))
for (const text of ['绑定到扫描托盘', '生成 DP 托盘并绑定', '确认封托']) assert.doesNotMatch(work, new RegExp(text))
assert.match(work, /class="task-summary-card"/)
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, /canSeal\(\)[\s\S]*?countedCartons[\s\S]*?totalCartons[\s\S]*?unboundCartons/)
assert.match(work, /allCartonsCounted\(\)[\s\S]*?countedCartons[\s\S]*?totalCartons/)
assert.match(work, /pallet\.status !== 'SEALED'/)
assert.match(work, /downloadDropshipPalletLabel/)
assert.match(work, /formatApiError/)
assert.doesNotMatch(work, /Number\(this\.warehouseId\)/)
assert.doesNotMatch(work, /taskId\s*\}\}|warehouseId\s*\}\}|inboundId\s*\}\}/)
})
+9 -7
View File
@@ -88,16 +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\)[\s\S]*?if \(!operation\.available\) return[\s\S]*?'一件代发合托': '\/pages\/dropship-tasks\/dropship-tasks'[\s\S]*?'出库拣货': '\/pages\/tasks\/tasks'[\s\S]*?'库位查询'/)
assert.match(home, /openOperation\(operation\)[\s\S]*?if \(!operation\.available\) return[\s\S]*?'一件代发': '\/pages\/dropship-tasks\/dropship-tasks'[\s\S]*?'出库拣货': '\/pages\/tasks\/tasks'[\s\S]*?'库位查询'/)
assert.match(home, /pages\/operation\/operation\?module=query&name=/)
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]*?'出库拣货': '\/pages\/tasks\/tasks'/)
assert.match(module, /openOperation\(operation\)[\s\S]*?if \(!operation\.available\) return[\s\S]*?'一件代发': '\/pages\/dropship-tasks\/dropship-tasks'[\s\S]*?'出库拣货': '\/pages\/tasks\/tasks'/)
assert.match(module, /pages\/operation\/operation\?module=\$\{encodeURIComponent\(this\.moduleKey\)\}&name=/)
assert.match(history, /作业记录/)
assert.match(profile, /个人中心/)
@@ -110,14 +110,14 @@ test('home exposes only the three confirmed operations and keeps hidden definiti
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: '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:一件代发合托',
'inbound:一件代发',
'outbound:出库拣货',
'query:库位查询'
])
@@ -178,7 +178,7 @@ test('picking cabinet selector synchronizes all ready server tasks by stable tas
const page = await read('../pages/picking/picking.vue')
assert.match(page, /listLoadingOrders/)
assert.match(page, /async onShow\(\)[\s\S]*?await this\.refreshAvailableBoxes\(\)[\s\S]*?await this\.syncRemoteTask\(\)/)
assert.match(page, /async refreshAvailableBoxes\(\)[\s\S]*?listLoadingOrders\(\{ page: 1, size: 50 \}\)/)
assert.match(page, /async refreshAvailableBoxes\(\)[\s\S]*?listLoadingOrders\(\{ page: 1, size: 50, warehouseId: session\.currentUser\?\.warehouseId \}\)/)
assert.match(page, /isSelectableTask\(task\)[\s\S]*?recordStatus === 'ACTIVE'[\s\S]*?\['UNASSIGNED', 'IN_PROGRESS', 'AWAITING_REVIEW'\]/)
assert.match(page, /existingByTaskId[\s\S]*?recordByTaskId[\s\S]*?orderedTaskIds[\s\S]*?this\.boxes = nextBoxes/)
assert.match(page, /selectedTaskId[\s\S]*?findIndex\(\(box\) => String\(box\.taskId \|\| ''\) === selectedTaskId\)/)
@@ -226,7 +226,9 @@ test('business dialogs use uni-ui popup components consistently', async () => {
assert.match(popupHeader, /aria-label="`关闭\$\{title\}`"/)
assert.match(popupHeader, /@tap="\$emit\('close'\)"/)
assert.match(header, /<uni-popup ref="menuPopup"[\s\S]*?:is-mask-click="true"/)
assert.match(header, /<PopupHeader title="功能菜单" @close="closeMenu"/)
assert.match(header, /<PopupHeader title="选择作业仓库" @close="closeMenu"/)
assert.match(header, /v-for="warehouse in warehouses"[\s\S]*?@tap="chooseWarehouse\(warehouse\)"/)
assert.doesNotMatch(header, />作业记录<|>个人中心</)
assert.doesNotMatch(header, /uni\.showActionSheet/)
assert.match(profile, /<uni-popup ref="logoutPopup" type="center"[\s\S]*?:is-mask-click="true"[\s\S]*?@change="handleLogoutChange"/)
assert.match(profile, /<PopupHeader title="确认退出登录?"[\s\S]*?@close="closeLogout"/)
+2
View File
@@ -10,6 +10,8 @@ test('PDA loading adapter uses only task-scoped cabinet evidence paths', () => {
for (const path of [
'/first-leg/pda-loading/tasks/pending'
]) assert.match(source, new RegExp(path.replace(/[?]/g, '\\?')))
assert.match(source, /\/first-leg\/pda-loading\/warehouses/)
assert.match(source, /query\.push\(`warehouseId=\$\{encodeURIComponent\(warehouseId\)\}`\)/)
assert.match(source, /first-leg\/pda-loading\/tasks\/\$\{encodeURIComponent\(taskId\)\}\/evidence/)
assert.match(source, /'Idempotency-Key'/)
assert.match(source, /PHOTO_STEP_1[\s\S]*PHOTO_STEP_4[\s\S]*SEAL/)