refactor: remove dropship printing from PDA
This commit is contained in:
@@ -58,7 +58,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 不展开全部托盘卡片,直接扫码并只展示当前操作托盘;全部预生成托盘码合并为每托一页的多页 A4 PDF,含 SKU 明细但不直接驱动打印机;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 托盘码,由 Admin 电脑端合并打印后贴到空托盘;PDA 不承载打印,只直接扫码并展示当前操作托盘;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/dropship-palletization/dropship-palletization.vue` | `stores/dropship-palletizing.js`、`utils/dropship-palletizing-state.js`;服务端契约见 `../../baiyue-firstleg/firstleg/src/main/resources/openapi/dropship-palletization-v1.yaml`,合同 `tests/dropship-palletizing-*.test.mjs` |
|
||||
|
||||
+1
-26
@@ -1,4 +1,4 @@
|
||||
import { apiDownload, apiRequest } from '@/api/http.js'
|
||||
import { apiRequest } from '@/api/http.js'
|
||||
import { stableOperationId } from '@/utils/idempotency.js'
|
||||
import { session } from '@/stores/session.js'
|
||||
|
||||
@@ -44,31 +44,6 @@ export function sealDropshipPallet(taskId, warehouseId, pallet) {
|
||||
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, { warehouseId, palletId: pallet.palletId, version: pallet.version }) } })
|
||||
}
|
||||
|
||||
export function downloadDropshipPalletLabel(taskId, warehouseId, palletId) {
|
||||
const baseUrl = String(session.apiBase || '/api/platform/v1').replace(/\/$/, '')
|
||||
const url = `${baseUrl}${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
|
||||
})
|
||||
}
|
||||
|
||||
export function downloadAllDropshipPalletLabels(taskId, warehouseId) {
|
||||
const baseUrl = String(session.apiBase || '/api/platform/v1').replace(/\/$/, '')
|
||||
const url = `${baseUrl}${ROOT}/tasks/${encodeURIComponent(taskId)}/pallets/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))) throw new Error('全部托盘码打印文件生成失败')
|
||||
return response.tempFilePath
|
||||
})
|
||||
}
|
||||
|
||||
function requireValue(value, message) {
|
||||
if (value === null || value === undefined || value === '') throw new Error(message)
|
||||
|
||||
+1
-1
@@ -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: '登记质检结果与异常信息' }
|
||||
|
||||
@@ -55,11 +55,9 @@
|
||||
|
||||
<view class="section-card">
|
||||
<view class="section-heading"><view><text class="section-title">2. 托盘操作</text><text class="section-desc">直接扫描托盘码,只显示当前操作的托盘;待绑托箱扫描目标托盘后批量归托</text></view><button class="text-button" :disabled="loading" @tap="loadTask">刷新</button></view>
|
||||
<view class="batch-print-row"><text>已生成 {{ task.generatedPalletCount || 0 }} 个托盘码</text><button class="text-button batch-print" :disabled="printing || !task.generatedPalletCount" @tap="printAllPallets">打开全部 A4 托盘单</button></view>
|
||||
<view v-if="currentPallet" class="pallet-card">
|
||||
<view class="pallet-head"><text class="pallet-code">{{ currentPallet.palletNo }}</text><text class="pallet-status" :class="currentPallet.status === 'SEALED' ? 'sealed' : ''">{{ currentPallet.status === 'SEALED' ? '已封托' : '当前托盘' }}</text></view>
|
||||
<text class="pallet-meta">{{ currentPallet.cartonCount || 0 }} 箱 · {{ currentPallet.skuCount || 0 }} SKU · {{ currentPallet.weightKg || 0 }} kg · {{ currentPallet.volumeCbm || 0 }} m³</text>
|
||||
<button v-if="currentPallet.palletId" class="secondary-button" :disabled="printing" @tap="printPallet(currentPallet)">{{ printing ? '正在生成 A4 文件…' : '打开当前托盘 A4 单' }}</button>
|
||||
<view v-for="item in (currentPalletDetails?.cartons || [])" :key="item.cartonId" class="carton-row"><text>{{ item.boxCode }}</text><button v-if="currentPallet.status === 'OPEN'" class="text-button" :disabled="submitting" @tap="moveCarton(item, null)">移回待绑托</button></view>
|
||||
<button v-if="currentPallet.palletId && currentPallet.status === 'OPEN'" class="primary-button" :disabled="!currentPallet.cartonCount || submitting" @tap="sealPallet(currentPallet)">封托</button>
|
||||
</view>
|
||||
@@ -72,7 +70,7 @@
|
||||
<script>
|
||||
import PortalHeader from '@/components/PortalHeader.vue'
|
||||
import ScanTargetInput from '@/components/ScanTargetInput.vue'
|
||||
import { bindDropshipPendingCartons, claimDropshipPalletizationTask, countDropshipCarton, downloadAllDropshipPalletLabels, downloadDropshipPalletLabel, getDropshipPalletizationTask, moveDropshipCarton, resolveDropshipCarton, sealDropshipPallet } from '@/api/pda-dropship.js'
|
||||
import { bindDropshipPendingCartons, claimDropshipPalletizationTask, countDropshipCarton, getDropshipPalletizationTask, moveDropshipCarton, resolveDropshipCarton, sealDropshipPallet } from '@/api/pda-dropship.js'
|
||||
import { session, requireSession } from '@/stores/session.js'
|
||||
import { formatApiError } from '@/utils/user-facing.js'
|
||||
import { stableOperationId } from '@/utils/idempotency.js'
|
||||
@@ -83,7 +81,7 @@
|
||||
|
||||
export default {
|
||||
components: { PortalHeader, ScanTargetInput },
|
||||
data() { return { user: session.currentUser, taskId: '', warehouseId: '', task: null, palletCode: '', selectedPalletId: '', selectedPalletNo: '', boxCode: '', carton: null, skuCounts: [], activeScan: 'boxCode', loading: false, submitting: false, printing: false, error: '', notice: '' } },
|
||||
data() { return { user: session.currentUser, taskId: '', warehouseId: '', task: null, palletCode: '', selectedPalletId: '', selectedPalletNo: '', boxCode: '', carton: null, skuCounts: [], activeScan: 'boxCode', loading: false, submitting: false, error: '', notice: '' } },
|
||||
computed: {
|
||||
statusLabel() { return this.task?.taskStatus === 'COMPLETED' ? '已完成' : (this.task?.taskStatus === 'CLAIMED' ? '作业中' : '待领取') },
|
||||
palletSetupReady() { return this.task?.palletSetupReady === true },
|
||||
@@ -141,36 +139,6 @@
|
||||
},
|
||||
continueOtherTask() { uni.redirectTo({ url: `/pages/dropship-tasks/dropship-tasks?warehouseId=${encodeURIComponent(this.warehouseId)}` }) },
|
||||
async sealPallet(pallet) { const result = await this.runMutation(() => sealDropshipPallet(this.taskId, this.warehouseId, pallet), `${pallet.palletNo} 已封托`); if (result && this.selectedPalletNo === pallet.palletNo) { this.selectedPalletId = ''; this.selectedPalletNo = ''; this.palletCode = '' } },
|
||||
async printPallet(pallet) {
|
||||
if (this.printing) 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} A4 托盘信息单已打开`
|
||||
} catch (error) { this.error = formatApiError(error, '托盘码打印失败') }
|
||||
finally { this.printing = false }
|
||||
},
|
||||
async printAllPallets() {
|
||||
if (this.printing) return
|
||||
this.printing = true; this.resetMessages()
|
||||
try {
|
||||
const filePath = await downloadAllDropshipPalletLabels(this.taskId, this.warehouseId)
|
||||
// #ifdef H5
|
||||
const link = document.createElement('a'); link.href = filePath; link.download = `${this.task.inboundCode}-全部托盘码.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 = `已打开 ${this.task.generatedPalletCount || 0} 个托盘码的 A4 文件`
|
||||
} catch (error) { this.error = formatApiError(error, '全部托盘码打印失败') }
|
||||
finally { this.printing = false }
|
||||
},
|
||||
async runMutation(action, message) {
|
||||
if (this.submitting) return null
|
||||
if (!isOnline()) { this.error = '当前网络不可用,请联网后刷新'; return null }
|
||||
@@ -209,6 +177,5 @@
|
||||
.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; }
|
||||
.batch-print-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 7px 0; border-top: 1px solid #edf1f5; color: #667085; font-size: 13px; }.batch-print { min-width: 148px; padding: 0 4px; font-weight: 700; }
|
||||
.palletization-page :deep(.scan-target-value) { font-size: 17px; }
|
||||
</style>
|
||||
|
||||
@@ -19,13 +19,9 @@ test('dropship palletization adapter uses only the dedicated production PDA cont
|
||||
const api = await read('../api/pda-dropship.js')
|
||||
for (const route of [
|
||||
'/tasks/pending', '/cartons/resolve', '/cartons/count',
|
||||
'/pallets/bind-pending', '/seal', '/print'
|
||||
'/pallets/bind-pending', '/seal'
|
||||
]) assert.match(api, new RegExp(route.replaceAll('/', '\\/')))
|
||||
assert.match(api, /apiDownload/)
|
||||
assert.match(api, /downloadAllDropshipPalletLabels/)
|
||||
assert.match(api, /pallets\/print\?warehouseId=/)
|
||||
assert.match(api, /application\\\/pdf/)
|
||||
assert.match(api, /session\.apiBase \|\| '\/api\/platform\/v1'/)
|
||||
assert.doesNotMatch(api, /apiDownload|downloadAllDropshipPalletLabels|pallets\/print/)
|
||||
assert.match(api, /tasks\/pending\?\$\{params\.join\('&'\)\}/)
|
||||
assert.match(api, /warehouseId=\$\{encodeURIComponent\(warehouseId\)\}/)
|
||||
assert.match(api, /operationId\('count'/)
|
||||
@@ -45,7 +41,7 @@ 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', '确认清点', '扫托盘', '可选', '差异说明', '托盘码', '打开全部 A4 托盘单', '封托']) assert.match(work, new RegExp(text))
|
||||
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))
|
||||
assert.match(work, /减少 \$\{line\.skuCode\} 清点数量/)
|
||||
assert.match(work, /增加 \$\{line\.skuCode\} 清点数量/)
|
||||
@@ -70,11 +66,9 @@ test('palletization UI accepts carton and pre-generated pallet scans in either o
|
||||
assert.match(work, /selectedPalletNo/)
|
||||
assert.match(work, /已选择仓库开放托盘/)
|
||||
assert.match(work, /直接扫描托盘码,只显示当前操作的托盘/)
|
||||
assert.match(work, /打开全部 A4 托盘单/)
|
||||
assert.match(work, /currentPalletDetails/)
|
||||
assert.doesNotMatch(work, /v-for="pallet in task\.pallets"/)
|
||||
assert.match(work, /downloadDropshipPalletLabel/)
|
||||
assert.match(work, /downloadAllDropshipPalletLabels/)
|
||||
assert.doesNotMatch(work, /downloadDropshipPalletLabel|downloadAllDropshipPalletLabels|打开当前托盘 A4 单|打开全部 A4 托盘单/)
|
||||
assert.match(work, /formatApiError/)
|
||||
assert.doesNotMatch(work, /Number\(this\.warehouseId\)/)
|
||||
assert.doesNotMatch(work, /taskId\s*\}\}|warehouseId\s*\}\}|inboundId\s*\}\}/)
|
||||
|
||||
@@ -28,11 +28,10 @@ test('formal palletization preserves both scan orders, moves and sealing guards'
|
||||
assert.match(page, /currentPalletDetails\?\.cartons/)
|
||||
assert.doesNotMatch(page, /模拟成功|actualSkuQuantity/)
|
||||
})
|
||||
test('palletization preserves compact multi-SKU input and printing', () => {
|
||||
test('palletization preserves compact multi-SKU input without desktop printing actions', () => {
|
||||
assert.match(page, /v-for="\(line, index\) in skuCounts"/)
|
||||
assert.match(page, /quantity-stepper/)
|
||||
assert.match(page, /printAllPallets/)
|
||||
assert.match(page, /printPallet/)
|
||||
assert.doesNotMatch(page, /printPallet|printAllPallets|打开当前托盘 A4 单|打开全部 A4 托盘单/)
|
||||
})
|
||||
test('count and move use the formal multi-SKU contract with versioned retries', () => {
|
||||
for (const path of ['cartons/count', 'pallets/bind-pending', '/move', '/seal']) assert.ok(api.includes(path))
|
||||
|
||||
Reference in New Issue
Block a user