diff --git a/AGENTS.md b/AGENTS.md index 4d69de1e..ba52b116 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,6 +51,7 @@ | 客户原始申报资料编辑与渠道变更 | `modules/unified-orders/components/OrderDeclarationEditorDialog.vue`、`api/unifiedOrders.ts`、`pages/UnifiedOrdersView.vue` | 使用订单版本和货物 `itemNo` 整体提交,不允许增删货物行;只编辑客户申报快照,不覆盖仓库/PDA 实测、测量审核或计费事实。渠道变化需二次确认并由 Backend `order/` 联查 `firstleg/` 的分柜、同柜、装柜与重新计价门禁;国内收货仓或中转仓变化必须重新下单 | | 入库扣货、解除与凭证历史 | `modules/unified-orders/pages/UnifiedOrdersView.vue`、`modules/inbounds/core/api.ts`、`types.ts` | 页面只消费服务端 `holdFlag/currentHoldReason/canHold/canRelease/actionBlockReason/history`;扣货与解除提交当前入库版本、幂等键、必填原因和可选凭证,凭证下载只走 Firstleg 授权入口。Backend `firstleg/` 的 `InboundHoldService`、`InboundHoldGuard`、`InboundHoldEvidenceService`,文件归 `file/` | | 入库、测量、称重库位状态、客户箱规策略与计价主数据 | 测量弹框/工作区 `modules/inbounds/measurement/InboundMeasurementDialog.vue`、`InboundMeasurementPanel.vue`,称重 `modules/inbounds/weight/components/WeightOcrPanel.vue`;主数据 `modules/fba-pricing/`、`modules/dimension-config/`、`modules/customers/components/CustomerBillingRulesPanel.vue`、`modules/fee-items/` | 客户固定四档箱规,默认计算方式为 `ADD/MULTIPLY`;测量行可选实际尺寸、箱规尺寸策略或客户默认尺寸系数,保存后以 Backend `firstleg/` 返回的外箱尺寸、体积重和计费基数为准,不在页面独立重算。费用确认的实际箱数、毛重和体积优先汇总当前 `actualMeasurementRows`,不得用可能滞后的工作区顶层合计覆盖明细。称重页在每张图片卡片只展示服务端 `locationCode/slotStatus/sharedOverflow`,不渲染或自行推导整票预留引导;删除图片须提示槽位仍归本票保留,不得按图片数组下标推导托盘或库位 | +| 一件代发托盘打印 | `modules/unified-orders/pages/UnifiedOrdersView.vue` 的“托盘打印”页签、`modules/inbounds/core/DropshipPalletPrintPanel.vue`、`core/api.ts` | 仅一件代发订单展示;测量审核预生成托盘码后由 Admin 电脑端打印,支持本票全部托盘合并打印和单托补打。PDA 只扫码组托,不承担 PDF 下载或打印机连接 | | 入库单扣货、解除、凭证与历史 | `modules/inbounds/pages/InboundOrdersView.vue`、详情工作区、`modules/inbounds/core/api.ts` | 列表展示服务端 `holdFlag/currentHoldReason` 并支持筛选;详情只按 `canHold/canRelease/actionBlockReason` 开放操作。扣货与解除提交当前 `version`、原因和独立幂等键,可选凭证先按明确客户与入库单上传;历史文件只调用专用下载入口,不把上传成功但未提交命令的文件冒充扣货证据 | | 客户专属价格簿、柜级阶梯运费与受控调价 | 列表/上传 `modules/fba-pricing/carrier-rates/pages/CarrierRatesView.vue`,版本详情/仓库映射/确认 `modules/fba-pricing/carrier-rates/pages/CarrierRateDetailView.vue`,路由 `modules/fba-pricing/routes.ts`;`modules/inbounds/fee/`、`modules/unified-orders/components/InboundFeeDetailManager.vue` | 列表与上传后按 `versionId` 进入 `fba-carrier-rate-detail` 独立详情页,价格行只读;回归入口 `modules/fba-pricing/carrier-rates/pages/CarrierRatesView.test.ts`。价格簿页面显式选择公共报价或具体客户;列表、上传、确认只作用于该 `customerId`,不得混合版本。计价优先消费客户专属 ACTIVE 版本,无专属版本才回退公共 ACTIVE;柜级阶梯金额和各客户分摊只展示服务端结果。调整计费量只能在服务端返回的上下 10% 范围内并提交当前费用版本、原因与二次确认;私人仓尾程派送费使用稳定 `PRIVATE_LAST_MILE_DELIVERY` 代码且只对 `PRIVATE` 目的地开放;一件代发须在预装确认前完成试算,计价后保持服务端 `WAIT_PALLETIZE` 状态 | | 财务、客户资金账户与柜级公共费用分摊 | `modules/finance/`,继续读该目录 `AGENTS.md`;客户账户 `pages/FinanceCustomerAccountsView.vue`,柜费 `modules/loading-lists/components/ContainerFeesPanel.vue`、`api/containerFees.ts` | `finance/`、`firstleg/`、`file/`、`docs/财务管理/`;实际、冻结、可用余额、授信与流水只消费 Finance 权威结果,充值审核和手工充扣携带版本与幂等键。跨客户公共柜费只按服务端柜内客户/订单事实分摊,金额必须守恒,创建与审核、驳回、调整、作废均整组原子处理 | diff --git a/src/modules/inbounds/core/DropshipPalletPrintPanel.test.ts b/src/modules/inbounds/core/DropshipPalletPrintPanel.test.ts new file mode 100644 index 00000000..9030ef22 --- /dev/null +++ b/src/modules/inbounds/core/DropshipPalletPrintPanel.test.ts @@ -0,0 +1,23 @@ +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { describe, expect, it } from 'vitest' + +const source = readFileSync(resolve(process.cwd(), 'src/modules/inbounds/core/DropshipPalletPrintPanel.vue'), 'utf8') + +describe('DropshipPalletPrintPanel', () => { + it('provides desktop batch printing and single-pallet reprinting', () => { + expect(source).toContain('一件代发托盘打印') + expect(source).toContain('打印全部托盘') + expect(source).toContain('单托补打') + expect(source).toContain('downloadAllDropshipPalletLabels') + expect(source).toContain('downloadDropshipPalletLabel') + expect(source).toContain('label="操作" width="120" fixed="left"') + }) + + it('explains the computer-first printing flow and covers loading, empty and error states', () => { + expect(source).toContain('在电脑端打印并贴到空托盘,再交由 PDA 扫码组托') + expect(source).toContain('v-loading="loading"') + expect(source).toContain('测量审核通过并生成托盘码后,可在这里打印') + expect(source).toContain('托盘数据暂时无法加载') + }) +}) diff --git a/src/modules/inbounds/core/DropshipPalletPrintPanel.vue b/src/modules/inbounds/core/DropshipPalletPrintPanel.vue new file mode 100644 index 00000000..52f4b8e5 --- /dev/null +++ b/src/modules/inbounds/core/DropshipPalletPrintPanel.vue @@ -0,0 +1,126 @@ + + + + + + + 一件代发托盘打印 + 测量审核生成托盘码后,在电脑端打印并贴到空托盘,再交由 PDA 扫码组托。 + + + 刷新 + 打印全部托盘 + + + + + + + + + + 单托补打 + + + + {{ statusLabel(row.status) }} + + + + + + + + + diff --git a/src/modules/inbounds/core/api.test.ts b/src/modules/inbounds/core/api.test.ts index 2c817de1..3fce2fed 100644 --- a/src/modules/inbounds/core/api.test.ts +++ b/src/modules/inbounds/core/api.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { http } from '@/shared/api/http' -import { downloadInboundCartonLabels, downloadInboundCartonLabelsBatch, downloadPlatformInboundHoldEvidence, getPlatformInbound, getPlatformInboundHold, holdPlatformInbound, listPlatformInboundDropshipPallets, listPlatformInbounds, releasePlatformInbound, uploadPlatformInboundHoldEvidence } from './api' +import { downloadAllDropshipPalletLabels, downloadDropshipPalletLabel, downloadInboundCartonLabels, downloadInboundCartonLabelsBatch, downloadPlatformInboundHoldEvidence, getPlatformInbound, getPlatformInboundHold, holdPlatformInbound, listPlatformInboundDropshipPallets, listPlatformInbounds, releasePlatformInbound, uploadPlatformInboundHoldEvidence } from './api' vi.mock('@/shared/api/http', () => ({ ApiResponseError: class ApiResponseError extends Error { @@ -87,6 +87,21 @@ describe('platform inbound query API', () => { expect(http.get).toHaveBeenCalledWith('/platform/v1/first-leg/pda-dropship/inbound-orders/19001/pallets', { params: { accountId: '10001' } }) }) + it('downloads one or all inbound-scoped dropship pallet documents for desktop printing', async () => { + const pdf = new Blob(['pdf'], { type: 'application/pdf' }) + vi.mocked(http.get).mockResolvedValue(pdf) + + await expect(downloadDropshipPalletLabel('10001', '19001', '21001')).resolves.toBe(pdf) + await expect(downloadAllDropshipPalletLabels('10001', '19001')).resolves.toBe(pdf) + + expect(http.get).toHaveBeenNthCalledWith(1, + '/platform/v1/first-leg/pda-dropship/inbound-orders/19001/pallets/21001/print', + { params: { accountId: '10001' }, responseType: 'blob', timeout: 60_000 }) + expect(http.get).toHaveBeenNthCalledWith(2, + '/platform/v1/first-leg/pda-dropship/inbound-orders/19001/pallets/print', + { params: { accountId: '10001' }, responseType: 'blob', timeout: 60_000 }) + }) + it('passes the implemented order, process, measurement and fee review filters without renaming them', async () => { vi.mocked(http.get).mockResolvedValue({ code: '200', message: 'success', data: { records: [], page: 1, size: 20, total: 0, pages: 0 }, diff --git a/src/modules/inbounds/core/api.ts b/src/modules/inbounds/core/api.ts index e72c6eda..f5e37f27 100644 --- a/src/modules/inbounds/core/api.ts +++ b/src/modules/inbounds/core/api.ts @@ -291,3 +291,10 @@ export async function downloadDropshipPalletLabel(tenantId: string, inboundId: s { params: { accountId: tenantId }, responseType: 'blob', timeout: 60_000 }, ) } + +export async function downloadAllDropshipPalletLabels(tenantId: string, inboundId: string) { + return http.get( + `/platform/v1/first-leg/pda-dropship/inbound-orders/${encodeURIComponent(inboundId)}/pallets/print`, + { params: { accountId: tenantId }, responseType: 'blob', timeout: 60_000 }, + ) +} diff --git a/src/modules/inbounds/index.ts b/src/modules/inbounds/index.ts index ba2c231d..1aadb9cf 100644 --- a/src/modules/inbounds/index.ts +++ b/src/modules/inbounds/index.ts @@ -1,5 +1,6 @@ export { getInboundMeasurement, saveInboundMeasurementDraft, updateMeasurementCalculationStrategy, updateMeasurementOuterDimensions } from './measurement/api' export { createInboundHoldIdempotencyKey, downloadInboundCartonLabelsBatch, downloadPlatformInboundHoldEvidence, getPlatformInboundHold, holdPlatformInbound, releasePlatformInbound, uploadPlatformInboundHoldEvidence } from './core/api' +export { default as DropshipPalletPrintPanel } from './core/DropshipPalletPrintPanel.vue' export { default as InboundMeasurementPanel } from './measurement/InboundMeasurementPanel.vue' export { default as InboundMeasurementDialog } from './measurement/InboundMeasurementDialog.vue' export { default as MeasurementEvidenceThumbnail } from './measurement/components/MeasurementEvidenceThumbnail.vue' diff --git a/src/modules/unified-orders/api/unifiedOrders.ts b/src/modules/unified-orders/api/unifiedOrders.ts index fc8c4162..3a8565bb 100644 --- a/src/modules/unified-orders/api/unifiedOrders.ts +++ b/src/modules/unified-orders/api/unifiedOrders.ts @@ -125,6 +125,7 @@ export interface UnifiedOrderResource { currentHoldReason: string | null companyName?: string | null businessType?: string | null + fulfillmentType?: string | null pickupMethod?: string | null requestedPickupAt: string | null pickupAt?: string | null diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts index 69e84246..10025636 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts @@ -7,6 +7,14 @@ const source = readFileSync(resolve(process.cwd(), 'src/modules/unified-orders/p const routeSource = readFileSync(resolve(process.cwd(), 'src/modules/unified-orders/routes.ts'), 'utf8').replace(/\r\n/g, '\n') describe('UnifiedOrdersView contract', () => { + it('exposes desktop pallet printing only for one-click dropship orders', () => { + expect(source).toContain("detail.value?.summary.fulfillmentType === 'ONE_CLICK_DROPSHIP'") + expect(source).toContain("{ key: 'dropship-print', label: '托盘打印', icon: '🖨️' }") + expect(source).toContain("activeDetailTab === 'dropship-print'") + expect(source).toContain(' { const openDialogSource = source.slice(source.indexOf('async function openFeeConfirmDialog()'), source.indexOf('async function executeConfirmAndSendFee()')) const sendFeeSource = source.slice(source.indexOf('async function executeConfirmAndSendFee()'), source.indexOf('function handleOrderSelectionChange')) diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.vue b/src/modules/unified-orders/pages/UnifiedOrdersView.vue index 7fea335a..3638b688 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.vue +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.vue @@ -78,6 +78,7 @@ import { InboundFeePanel, InboundMeasurementDialog, MeasurementEvidenceThumbnail, + DropshipPalletPrintPanel, sendInboundFee, type InboundFeeSnapshot, type InboundMeasurementDetail, @@ -324,15 +325,17 @@ const canReleaseInbound = computed(() => Boolean( && holdDetail.value?.canRelease, )) -const detailTabs = [ +const isDropshipOrder = computed(() => detail.value?.summary.fulfillmentType === 'ONE_CLICK_DROPSHIP') +const detailTabs = computed(() => [ { key: 'basic', label: '基本信息', icon: '📋' }, { key: 'cargo', label: '货品信息', icon: '📦' }, { key: 'fees', label: '费用信息', icon: '💰' }, { key: 'history', label: '状态历史', icon: '📝' }, { key: 'measurement', label: '测量数据', icon: '📐' }, + ...(isDropshipOrder.value ? [{ key: 'dropship-print', label: '托盘打印', icon: '🖨️' }] : []), { key: 'other', label: '其他', icon: '🗂️' }, { key: 'fulfillment', label: '拆分履约', icon: '🧩' }, -] +]) const customerMeasurementColumns = [ { key: 'itemNo', label: '#' }, { key: 'lengthCm', label: '箱规长(cm)' }, { key: 'widthCm', label: '箱规宽(cm)' }, @@ -2396,6 +2399,17 @@ onBeforeUnmount(() => { + + + + + + {{ detail.summary.measurement?.completedAt ? `测量完成于 ${formatDate(detail.summary.measurement.completedAt)}` : '尚未完成测量' }}
测量审核生成托盘码后,在电脑端打印并贴到空托盘,再交由 PDA 扫码组托。
{{ detail.summary.measurement?.completedAt ? `测量完成于 ${formatDate(detail.summary.measurement.completedAt)}` : '尚未完成测量' }}