diff --git a/AGENTS.md b/AGENTS.md index 4092cae..15a2054 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` 与本文件。 @@ -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` | diff --git a/README.md b/README.md index 581186c..f604236 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ 国内货柜履约装柜取证已具备接口适配基础:拣货页展示空仓、半仓、满仓、锁仓和带锁码照五类证据,经 File 服务上传取得真实 `fileId`,并展示待上传、上传中、失败、成功和重试状态。产品已确认 PDA 只能现场拍照,Admin 可人工上传;每类至少 1 张有效照片、同类型可多张追加,错误或重复照片只作废不硬删除。当前页面和上传队列仍按会话内单类型记录设计,尚未完整达到“多图追加+作废审计”契约。 -登录后默认进入首页,不再直接进入拣货。首页当前只展示“入库 → 一件代发合托”“出库 → 出库拣货”和“查询 → 库位查询”;其余业务入口保留在数据、页面和路由中,通过 `visible` 隐藏,不从源码删除。作业记录收纳在左上角功能菜单,个人中心从右上角人员入口进入。 +登录后默认进入首页,不再直接进入拣货。首页当前只展示“入库 → 一件代发”“出库 → 出库拣货”和“查询 → 库位查询”;其余业务入口保留在数据、页面和路由中,通过 `visible` 隐藏,不从源码删除。首页左上角从 Warehouse 主档选择启用国内仓,首次未选择时自动打开;一件代发与出库拣货待办都按所选仓库从服务端查询。首页不再展示作业记录和个人中心入口,对应页面与路由仍保留。 ## 页面与状态结构 diff --git a/api/pda-dropship.js b/api/pda-dropship.js index 52ed010..d73f8da 100644 --- a/api/pda-dropship.js +++ b/api/pda-dropship.js @@ -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 + }) } diff --git a/api/pda-loading.js b/api/pda-loading.js index 4983efb..14814b1 100644 --- a/api/pda-loading.js +++ b/api/pda-loading.js @@ -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) => ({ diff --git a/components/PortalHeader.vue b/components/PortalHeader.vue index 52b5cc8..7e47b80 100644 --- a/components/PortalHeader.vue +++ b/components/PortalHeader.vue @@ -2,7 +2,7 @@ - + {{ title }} @@ -12,9 +12,11 @@ - - - + + 正在加载仓库… + {{ warehouseError }} + 暂无可用国内仓 + @@ -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)) } diff --git a/data/demo-data.js b/data/demo-data.js index 822f80f..abb6abd 100644 --- a/data/demo-data.js +++ b/data/demo-data.js @@ -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: '登记质检结果与异常信息' } diff --git a/pages.json b/pages.json index e3c44d2..56997eb 100644 --- a/pages.json +++ b/pages.json @@ -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" } }, diff --git a/pages/dropship-palletization/dropship-palletization.vue b/pages/dropship-palletization/dropship-palletization.vue index 7b782ef..7264091 100644 --- a/pages/dropship-palletization/dropship-palletization.vue +++ b/pages/dropship-palletization/dropship-palletization.vue @@ -1,12 +1,12 @@