feat: support dropship pallet loading
This commit is contained in:
@@ -19,7 +19,8 @@ describe('InboundWorkflowWorkspace', () => {
|
||||
expect(source).toContain('label="测量审核"')
|
||||
expect(source).toContain('label="费用信息"')
|
||||
expect(source).toContain('label="状态历史"')
|
||||
expect(source).toContain('label="其他记录"')
|
||||
expect(source).toContain("detail.summary.fulfillmentType === 'ONE_CLICK_DROPSHIP' ? '清点组托与其他记录' : '其他记录'")
|
||||
expect(source).toContain('listPlatformInboundDropshipPallets(props.customerId, props.inboundId)')
|
||||
})
|
||||
|
||||
it('opens measurement editing and review in prototype-sized centered dialogs', () => {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import StatusBadge from '@/shared/components/StatusBadge.vue'
|
||||
import { getPlatformInbound } from '../core/api'
|
||||
import { getPlatformInbound, listPlatformInboundDropshipPallets } from '../core/api'
|
||||
import InboundHistoryPanel from '../core/InboundHistoryPanel.vue'
|
||||
import InboundSummaryPanel from '../core/InboundSummaryPanel.vue'
|
||||
import type { InboundOrderDetail } from '../core/types'
|
||||
@@ -37,6 +37,7 @@ let disposed = false
|
||||
|
||||
const title = computed(() => detail.value?.summary.inboundCode || '入仓码待补充')
|
||||
const canViewMeasurement = computed(() => access.can('platform:firstleg:inbound:measure:view'))
|
||||
const canViewDropshipPallets = computed(() => access.can('platform:firstleg:pda:view'))
|
||||
const statusLabel = computed(() => inboundStatusLabel(detail.value?.summary.processStatus, inboundProcessStatusLabels, '未知状态'))
|
||||
|
||||
async function loadDetail() {
|
||||
@@ -51,6 +52,9 @@ async function loadDetail() {
|
||||
loadError.value = null
|
||||
try {
|
||||
const response = await getPlatformInbound(props.customerId, props.inboundId)
|
||||
if (response.summary.fulfillmentType === 'ONE_CLICK_DROPSHIP' && canViewDropshipPallets.value) {
|
||||
response.dropshipPallets = await listPlatformInboundDropshipPallets(props.customerId, props.inboundId)
|
||||
}
|
||||
if (disposed || serial !== requestSerial) return
|
||||
detail.value = response
|
||||
} catch (error) {
|
||||
@@ -174,12 +178,13 @@ onBeforeUnmount(() => {
|
||||
:sections="['history']"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="其他记录" name="records" lazy>
|
||||
<el-tab-pane :label="detail.summary.fulfillmentType === 'ONE_CLICK_DROPSHIP' ? '清点组托与其他记录' : '其他记录'" name="records" lazy>
|
||||
<InboundHistoryPanel
|
||||
:pallet-locations="detail.palletLocations"
|
||||
:dropship-pallets="detail.dropshipPallets"
|
||||
:customer-id="customerId"
|
||||
:inbound-id="inboundId"
|
||||
:can-view-dropship="canViewDropshipPallets"
|
||||
:files="detail.files"
|
||||
:internal-notes="detail.internalNotes"
|
||||
:status-history="[]"
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
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/InboundHistoryPanel.vue'), 'utf8')
|
||||
|
||||
describe('InboundHistoryPanel dropship pallet result', () => {
|
||||
it('shows the frozen pallet, carton and discrepancy facts', () => {
|
||||
for (const marker of [
|
||||
'一件代发清点与组托结果', 'pallet.palletNo', 'pallet.status', 'pallet.cartonCount',
|
||||
'pallet.skuCount', 'pallet.weightKg', 'pallet.volumeCbm', 'pallet.discrepancyCartons', 'pallet.sealedAt',
|
||||
'item.inboundCode', 'item.skuCode', 'item.cargoName', 'item.skuQuantity', 'item.cartonCount', 'item.cartonMarks',
|
||||
]) expect(source).toContain(marker)
|
||||
expect(source).toContain("SEALED: '已封托'")
|
||||
})
|
||||
|
||||
it('keeps the existing scoped pallet-label action available', () => {
|
||||
expect(source).toContain('downloadDropshipPalletLabel(props.customerId, props.inboundId, pallet.palletId)')
|
||||
expect(source).toContain('打印托盘唛')
|
||||
})
|
||||
})
|
||||
@@ -9,6 +9,7 @@ const props = withDefaults(defineProps<{
|
||||
dropshipPallets?: InboundDropshipPallet[]
|
||||
customerId?: string
|
||||
inboundId?: string
|
||||
canViewDropship?: boolean
|
||||
files: InboundFileSummary[]
|
||||
internalNotes: InboundInternalNote[]
|
||||
statusHistory: InboundStatusHistory[]
|
||||
@@ -16,6 +17,7 @@ const props = withDefaults(defineProps<{
|
||||
sections?: Array<'dropship' | 'allocations' | 'files' | 'notes' | 'history' | 'remark'>
|
||||
}>(), {
|
||||
dropshipPallets: () => [],
|
||||
canViewDropship: true,
|
||||
sections: () => ['dropship', 'allocations', 'files', 'notes', 'history', 'remark'],
|
||||
})
|
||||
|
||||
@@ -30,6 +32,9 @@ function date(value: string | null) {
|
||||
const parsed = new Date(value)
|
||||
return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleString('zh-CN', { hour12: false })
|
||||
}
|
||||
function dropshipPalletStatus(value: string) {
|
||||
return ({ OPEN: '清点组托中', SEALED: '已封托' } as Record<string, string>)[value] || value || '状态未知'
|
||||
}
|
||||
async function printPallet(pallet: InboundDropshipPallet) {
|
||||
if (!props.customerId || !props.inboundId || !pallet.palletNo) return
|
||||
try {
|
||||
@@ -44,9 +49,35 @@ async function printPallet(pallet: InboundDropshipPallet) {
|
||||
<template>
|
||||
<div class="history-panel">
|
||||
<section v-if="shows('dropship')" class="history-section">
|
||||
<div class="section-heading"><h3>一件代发绑定托盘</h3><span>{{ dropshipPallets.length }} 个托盘</span></div>
|
||||
<div v-if="dropshipPallets.length" class="simple-list"><div v-for="pallet in dropshipPallets" :key="pallet.palletId"><strong>{{ value(pallet.palletNo) }} · {{ pallet.cartonCount }} 箱 <el-button v-if="pallet.palletNo" link type="primary" @click="printPallet(pallet)">打印托盘唛</el-button></strong><span v-for="item in pallet.items" :key="`${pallet.palletId}-${item.inboundDetailId}`">{{ item.inboundCode }} · SKU {{ item.skuCode }} · {{ item.cartonCount }} 箱</span></div></div>
|
||||
<p v-else class="muted-empty">暂无一件代发托盘绑定</p>
|
||||
<div class="section-heading"><h3>一件代发清点与组托结果</h3><span>{{ dropshipPallets.length }} 个托盘</span></div>
|
||||
<el-alert v-if="!canViewDropship" title="当前账号缺少一件代发 PDA 查看权限,无法读取清点组托结果。" type="warning" :closable="false" show-icon />
|
||||
<div v-else-if="dropshipPallets.length" class="dropship-pallet-list">
|
||||
<article v-for="pallet in dropshipPallets" :key="pallet.palletId" class="dropship-pallet-card">
|
||||
<header>
|
||||
<div><strong>{{ value(pallet.palletNo) }}</strong><el-tag size="small" :type="pallet.status === 'SEALED' ? 'success' : 'warning'" effect="plain">{{ dropshipPalletStatus(pallet.status) }}</el-tag></div>
|
||||
<el-button v-if="pallet.palletNo" link type="primary" @click="printPallet(pallet)">打印托盘唛</el-button>
|
||||
</header>
|
||||
<dl class="dropship-pallet-summary">
|
||||
<div><dt>箱数</dt><dd>{{ pallet.cartonCount }}</dd></div>
|
||||
<div><dt>SKU 数</dt><dd>{{ pallet.skuCount }}</dd></div>
|
||||
<div><dt>毛重</dt><dd>{{ number(pallet.weightKg) }} kg</dd></div>
|
||||
<div><dt>体积</dt><dd>{{ number(pallet.volumeCbm) }} m³</dd></div>
|
||||
<div><dt>差异箱数</dt><dd><el-tag size="small" :type="pallet.discrepancyCartons ? 'warning' : 'success'">{{ pallet.discrepancyCartons }}</el-tag></dd></div>
|
||||
<div><dt>封托时间</dt><dd>{{ date(pallet.sealedAt) }}</dd></div>
|
||||
</dl>
|
||||
<div class="dropship-item-table">
|
||||
<div class="dropship-item-row dropship-item-row--header"><span>入仓码</span><span>SKU / 货物</span><span>商品数量</span><span>箱数</span><span>箱码</span></div>
|
||||
<div v-for="item in pallet.items" :key="`${pallet.palletId}-${item.inboundDetailId}`" class="dropship-item-row">
|
||||
<span>{{ value(item.inboundCode) }}</span>
|
||||
<span><strong>{{ value(item.skuCode) }}</strong><small>{{ value(item.cargoName) }}</small></span>
|
||||
<span>{{ item.skuQuantity }}</span>
|
||||
<span>{{ item.cartonCount }}</span>
|
||||
<span>{{ item.cartonMarks.length ? item.cartonMarks.join('、') : '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<p v-else class="muted-empty">暂无一件代发清点与组托结果</p>
|
||||
</section>
|
||||
<section v-if="shows('allocations')" class="history-section">
|
||||
<div class="section-heading"><h3>托盘与库位</h3><span>{{ palletLocations.length }} 条分配</span></div>
|
||||
@@ -93,6 +124,25 @@ async function printPallet(pallet: InboundDropshipPallet) {
|
||||
.allocation-location { overflow-wrap: anywhere; color: var(--cc-primary); text-align: right; }
|
||||
.simple-list > div { padding: 10px 12px; border-bottom: 1px solid #edf0f4; font-size: 12px; }
|
||||
.simple-list strong, .simple-list span { overflow-wrap: anywhere; }
|
||||
.dropship-pallet-list { display: grid; gap: 12px; }
|
||||
.dropship-pallet-card { min-width: 0; overflow: hidden; border: 1px solid var(--cc-border); border-radius: 6px; background: #fff; }
|
||||
.dropship-pallet-card > header { display: flex; min-height: 42px; align-items: center; justify-content: space-between; gap: 12px; padding: 8px 12px; border-bottom: 1px solid #edf0f4; background: #f8fafc; }
|
||||
.dropship-pallet-card > header > div { display: flex; min-width: 0; align-items: center; gap: 8px; }
|
||||
.dropship-pallet-card > header strong { overflow-wrap: anywhere; color: #172033; font-size: 13px; }
|
||||
.dropship-pallet-summary { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); margin: 0; border-bottom: 1px solid #edf0f4; }
|
||||
.dropship-pallet-summary > div { min-width: 0; padding: 8px 12px; border-right: 1px solid #edf0f4; }
|
||||
.dropship-pallet-summary > div:last-child { border-right: 0; }
|
||||
.dropship-pallet-summary dt { color: var(--cc-muted); font-size: 11px; }
|
||||
.dropship-pallet-summary dd { margin: 3px 0 0; overflow-wrap: anywhere; color: var(--cc-text); font-size: 12px; font-weight: 650; }
|
||||
.dropship-item-table { min-width: 680px; }
|
||||
.dropship-pallet-card { overflow-x: auto; }
|
||||
.dropship-item-row { display: grid; grid-template-columns: minmax(140px, 1fr) minmax(180px, 1.2fr) 100px 70px minmax(220px, 1.5fr); min-width: 800px; border-bottom: 1px solid #edf0f4; }
|
||||
.dropship-item-row:last-child { border-bottom: 0; }
|
||||
.dropship-item-row > span { min-width: 0; padding: 8px 12px; overflow-wrap: anywhere; color: var(--cc-text); font-size: 12px; }
|
||||
.dropship-item-row > span + span { border-left: 1px solid #edf0f4; }
|
||||
.dropship-item-row small { display: block; margin-top: 3px; color: var(--cc-muted); font-size: 11px; }
|
||||
.dropship-item-row--header { background: #f8fafc; }
|
||||
.dropship-item-row--header > span { color: var(--cc-muted); font-size: 11px; font-weight: 650; }
|
||||
.muted-empty { margin: 0; padding: 18px 12px; border: 1px dashed var(--cc-border); border-radius: 6px; background: #f8fafc; color: var(--cc-muted); font-size: 12px; text-align: center; }
|
||||
.timeline { display: grid; gap: 0; margin: 0; padding: 0; list-style: none; }
|
||||
.timeline li { display: grid; grid-template-columns: 16px 1fr; gap: 10px; min-width: 0; padding-bottom: 16px; }
|
||||
@@ -101,5 +151,6 @@ async function printPallet(pallet: InboundDropshipPallet) {
|
||||
.timeline strong, .timeline span, .timeline small { display: block; overflow-wrap: anywhere; }
|
||||
.timeline span, .timeline small { margin-top: 3px; color: var(--cc-muted); font-size: 12px; line-height: 1.5; }
|
||||
.remark-section p { margin: 0; padding: 12px; border-radius: 6px; background: #f8fafc; color: var(--cc-text); font-size: 13px; line-height: 1.6; white-space: pre-wrap; }
|
||||
@media (max-width: 720px) { .dropship-pallet-summary { grid-template-columns: 1fr; } .dropship-pallet-summary > div { border-right: 0; border-bottom: 1px solid #edf0f4; } .dropship-pallet-summary > div:last-child { border-bottom: 0; } }
|
||||
@media (max-width: 480px) { .allocation-row { grid-template-columns: 1fr auto; } .allocation-location { grid-column: 1 / -1; text-align: left; } }
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { http } from '@/shared/api/http'
|
||||
import { downloadInboundCartonLabels, downloadPlatformInboundHoldEvidence, getPlatformInbound, getPlatformInboundHold, holdPlatformInbound, listPlatformInbounds, releasePlatformInbound, uploadPlatformInboundHoldEvidence } from './api'
|
||||
import { downloadInboundCartonLabels, downloadPlatformInboundHoldEvidence, getPlatformInbound, getPlatformInboundHold, holdPlatformInbound, listPlatformInboundDropshipPallets, listPlatformInbounds, releasePlatformInbound, uploadPlatformInboundHoldEvidence } from './api'
|
||||
|
||||
vi.mock('@/shared/api/http', () => ({
|
||||
ApiResponseError: class ApiResponseError extends Error {
|
||||
@@ -73,6 +73,20 @@ describe('platform inbound query API', () => {
|
||||
expect(http.get).toHaveBeenCalledWith('/platform/inbounds/19001', { params: { tenantId: '10001' } })
|
||||
})
|
||||
|
||||
it('loads and normalizes the inbound-scoped dropship pallet result', async () => {
|
||||
vi.mocked(http.get).mockResolvedValue({ code: '200', data: [{
|
||||
palletId: 9007199254740993n, palletNo: 'PLT-001', status: 'SEALED', cartonCount: '2', skuCount: '1', version: '3',
|
||||
weightKg: '20.5', volumeCbm: '0.88', discrepancyCartons: '1', sealedAt: '2026-09-08T10:00:00',
|
||||
items: [{ inboundId: 19001, inboundCode: 'RK001', inboundDetailId: 9007199254740994n, skuCode: 'SKU001', cargoName: '服装', cartonCount: '2', skuQuantity: '19', cartonMarks: ['BOX-001', 'BOX-002'] }],
|
||||
}] })
|
||||
|
||||
await expect(listPlatformInboundDropshipPallets('10001', '19001')).resolves.toMatchObject([{
|
||||
palletId: '9007199254740993', weightKg: 20.5, volumeCbm: 0.88, discrepancyCartons: 1,
|
||||
items: [{ inboundId: '19001', inboundDetailId: '9007199254740994', cartonCount: 2, skuQuantity: 19, cartonMarks: ['BOX-001', 'BOX-002'] }],
|
||||
}])
|
||||
expect(http.get).toHaveBeenCalledWith('/platform/v1/first-leg/pda-dropship/inbound-orders/19001/pallets', { params: { accountId: '10001' } })
|
||||
})
|
||||
|
||||
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 },
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
InboundPageResult,
|
||||
InboundPalletLocation,
|
||||
InboundStatusHistory,
|
||||
InboundDropshipPallet,
|
||||
} from './types'
|
||||
|
||||
const basePath = '/platform/inbounds'
|
||||
@@ -102,11 +103,7 @@ function normalizeDetail(raw: InboundOrderDetail): InboundOrderDetail {
|
||||
return {
|
||||
...raw,
|
||||
summary: normalizeInboundSummary(raw.summary),
|
||||
dropshipPallets: (raw.dropshipPallets || []).map((pallet) => ({
|
||||
...pallet,
|
||||
palletId: id(pallet.palletId) || '',
|
||||
items: (pallet.items || []).map((item) => ({ ...item, inboundId: id(item.inboundId) || '', inboundDetailId: id(item.inboundDetailId) || '' })),
|
||||
})),
|
||||
dropshipPallets: [],
|
||||
palletLocations: (raw.palletLocations || []).map(normalizePallet),
|
||||
files: (raw.files || []).map(normalizeFile),
|
||||
internalNotes: (raw.internalNotes || []).map(normalizeNote),
|
||||
@@ -114,6 +111,29 @@ function normalizeDetail(raw: InboundOrderDetail): InboundOrderDetail {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeDropshipPallet(raw: InboundDropshipPallet): InboundDropshipPallet {
|
||||
return {
|
||||
...raw,
|
||||
palletId: id(raw.palletId) || '',
|
||||
palletNo: raw.palletNo || null,
|
||||
cartonCount: integerValue(raw.cartonCount) || 0,
|
||||
skuCount: integerValue(raw.skuCount) || 0,
|
||||
version: integerValue(raw.version) || 0,
|
||||
weightKg: numberValue(raw.weightKg),
|
||||
volumeCbm: numberValue(raw.volumeCbm),
|
||||
discrepancyCartons: integerValue(raw.discrepancyCartons) || 0,
|
||||
sealedAt: raw.sealedAt || null,
|
||||
items: (raw.items || []).map((item) => ({
|
||||
...item,
|
||||
inboundId: id(item.inboundId) || '',
|
||||
inboundDetailId: id(item.inboundDetailId) || '',
|
||||
cartonCount: integerValue(item.cartonCount) || 0,
|
||||
skuQuantity: integerValue(item.skuQuantity) || 0,
|
||||
cartonMarks: item.cartonMarks || [],
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeHoldDetail(raw: InboundHoldDetail): InboundHoldDetail {
|
||||
return {
|
||||
...raw,
|
||||
@@ -161,6 +181,14 @@ export async function getPlatformInbound(tenantId: string, inboundId: string) {
|
||||
return normalizeDetail(unwrapApiResponse(response))
|
||||
}
|
||||
|
||||
export async function listPlatformInboundDropshipPallets(accountId: string, inboundId: string) {
|
||||
const response = await http.get<ApiEnvelope<InboundDropshipPallet[]>, ApiEnvelope<InboundDropshipPallet[]>>(
|
||||
`/platform/v1/first-leg/pda-dropship/inbound-orders/${encodeURIComponent(inboundId)}/pallets`,
|
||||
{ params: { accountId } },
|
||||
)
|
||||
return (unwrapApiResponse(response) || []).map(normalizeDropshipPallet)
|
||||
}
|
||||
|
||||
export async function getPlatformInboundHold(tenantId: string, inboundId: string) {
|
||||
const response = await http.get<ApiEnvelope<InboundHoldDetail>, ApiEnvelope<InboundHoldDetail>>(
|
||||
`${basePath}/${encodeURIComponent(inboundId)}/hold`,
|
||||
|
||||
@@ -36,6 +36,7 @@ export interface InboundOrderSummary {
|
||||
deliveryWarehouseId: string | null
|
||||
deliveryWarehouseName: string | null
|
||||
businessType: string | null
|
||||
fulfillmentType: 'STANDARD' | 'ONE_CLICK_DROPSHIP' | string | null
|
||||
channelCode: string | null
|
||||
channelName: string | null
|
||||
taxMode: string | null
|
||||
@@ -161,12 +162,26 @@ export interface InboundHoldEvidenceUpload {
|
||||
export interface InboundDropshipPallet {
|
||||
palletId: string
|
||||
palletNo: string | null
|
||||
palletStatus: string
|
||||
status: 'OPEN' | 'SEALED'
|
||||
cartonCount: number
|
||||
skuCount: number
|
||||
version: number
|
||||
items: Array<{ inboundId: string; inboundCode: string; inboundDetailId: string; skuCode: string; cargoName: string | null; cartonCount: number; cartonMarks: string[] }>
|
||||
completedAt: string | null
|
||||
weightKg: number | null
|
||||
volumeCbm: number | null
|
||||
discrepancyCartons: number
|
||||
sealedAt: string | null
|
||||
items: InboundDropshipSkuItem[]
|
||||
}
|
||||
|
||||
export interface InboundDropshipSkuItem {
|
||||
inboundId: string
|
||||
inboundCode: string
|
||||
inboundDetailId: string
|
||||
skuCode: string
|
||||
cargoName: string | null
|
||||
cartonCount: number
|
||||
skuQuantity: number
|
||||
cartonMarks: string[]
|
||||
}
|
||||
|
||||
export interface InboundPageResult {
|
||||
|
||||
@@ -43,7 +43,7 @@ const summary: InboundOrderSummary = {
|
||||
orderStatusSnapshot: 'APPROVED', measureReviewStatus: 'PENDING', feeStatus: 'UNPRICED', feeReviewStatus: null,
|
||||
notificationStatus: null, version: 1, expectedArrivalAt: '2026-08-04T10:00:00', customerId: '10001', customerName: '示例客户',
|
||||
companyId: '10001', companyName: '示例企业', warehouseId: '20001', warehouseName: '义乌收货仓', deliveryWarehouseId: null,
|
||||
deliveryWarehouseName: null, businessType: 'FBA', channelCode: 'SEA', channelName: '海运', taxMode: 'DDP', containerMode: null,
|
||||
deliveryWarehouseName: null, businessType: 'FBA', fulfillmentType: 'STANDARD', channelCode: 'SEA', channelName: '海运', taxMode: 'DDP', containerMode: null,
|
||||
customsInspection: false, destinationType: 'FBA', destinationWarehouseId: '30001', destinationCode: 'ONT8', destinationName: 'ONT8',
|
||||
holdFlag: false, currentHoldReason: null, totalPackageCount: 6, actualGrossWeightKg: 120, actualNetWeightKg: 110,
|
||||
actualVolumeCbm: 1.2, volumeWeightKg: 200, chargeableWeightKg: 200, chargeableVolumeCbm: null, palletCount: 1, locationCodes: 'A-01',
|
||||
|
||||
Reference in New Issue
Block a user