优化
This commit is contained in:
@@ -20,12 +20,6 @@ export async function reviewCargoDiscard(discardNo: string, decision: 'APPROVE'
|
||||
const response = await http.post<ApiEnvelope<CargoDiscardDetail>, ApiEnvelope<CargoDiscardDetail>>(`/platform/v1/last-leg/cargo-discards/${encodeURIComponent(discardNo)}/review`, { decision, remark }, { params: customerId ? { customerId } : undefined })
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
export async function completeCargoDiscard(discardNo: string, customerId?: string) {
|
||||
const response = await http.post<ApiEnvelope<CargoDiscardDetail>, ApiEnvelope<CargoDiscardDetail>>(`/platform/v1/last-leg/cargo-discards/${encodeURIComponent(discardNo)}/complete`, undefined, { params: customerId ? { customerId } : undefined })
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
|
||||
export function downloadCargoDiscardPhoto(discardNo: string, fileId: string, customerId: string) {
|
||||
return http.get<Blob, Blob>(`/platform/v1/last-leg/cargo-discards/${encodeURIComponent(discardNo)}/photos/files/${encodeURIComponent(fileId)}/download`, { params: { customerId }, responseType: 'blob' })
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { http } from '@/shared/api/http'
|
||||
import { allocateWarehouseTransfer, getWarehouseTransfer, listWarehouseTransfers, reviewWarehouseTransfer, outboundWarehouseTransfer, receiveWarehouseTransfer, listTransferReceivingWarehouses } from './warehouseTransfers'
|
||||
import { allocateWarehouseTransfer, getWarehouseTransferCartonLabel, getWarehouseTransfer, listWarehouseTransfers, reviewWarehouseTransfer, outboundWarehouseTransfer, receiveWarehouseTransfer, listTransferReceivingWarehouses } from './warehouseTransfers'
|
||||
|
||||
vi.mock('@/shared/api/http', async (importOriginal) => ({ ...(await importOriginal<typeof import('@/shared/api/http')>()), http: { get: vi.fn(), post: vi.fn() } }))
|
||||
|
||||
describe('warehouseTransfers API', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
it('loads server-rendered carton labels using the source transfer and customer scope', async () => {
|
||||
const label = { transferNo: 'WT/1', cartons: [{ cartonMark: 'BOX-1' }], printableHtml: '<html>BOX-1</html>' }
|
||||
vi.mocked(http.get).mockResolvedValue({ code: '200', data: label })
|
||||
expect(await getWarehouseTransferCartonLabel('customer-1', 'WT/1')).toEqual(label)
|
||||
expect(http.get).toHaveBeenCalledExactlyOnceWith('/platform/v1/last-leg/warehouse-transfers/WT%2F1/carton-label', { params: { customerId: 'customer-1' } })
|
||||
})
|
||||
it('uses the resource-role receiving options contract, never warehouse master list', async () => {
|
||||
vi.mocked(http.get).mockResolvedValue({ code: '200', data: { records: [{ id: 'NY', customerId: 0, warehouseCode: 'NY', warehouseName: '纽约', warehouseType: 'OVERSEAS', status: 'ACTIVE' }], total: 1, page: 1, size: 200, pages: 1 } })
|
||||
expect((await listTransferReceivingWarehouses()).records[0]?.customerId).toBe('0')
|
||||
|
||||
@@ -35,11 +35,21 @@ export interface WarehouseTransferItemDto {
|
||||
id: string
|
||||
skuCode: string
|
||||
productNameZh: string
|
||||
quantity?: number | string | null
|
||||
packageCount: number
|
||||
shippingMark?: string | null
|
||||
}
|
||||
|
||||
export interface WarehouseTransferCartonDto {
|
||||
transferItemId?: string | null
|
||||
transferPalletId?: string | null
|
||||
length?: number | string | null
|
||||
width?: number | string | null
|
||||
height?: number | string | null
|
||||
weight?: number | string | null
|
||||
quantity?: number | string | null
|
||||
countedAt?: string | null
|
||||
discrepancyReason?: string | null
|
||||
cartonMark?: string | null
|
||||
customerCartonMark?: string | null
|
||||
cartonIndex: number
|
||||
@@ -47,6 +57,19 @@ export interface WarehouseTransferCartonDto {
|
||||
status: string
|
||||
}
|
||||
|
||||
export interface WarehouseTransferCartonLabelDto {
|
||||
transferNo: string
|
||||
cartons: WarehouseTransferCartonDto[]
|
||||
printableHtml: string
|
||||
}
|
||||
|
||||
export async function getWarehouseTransferCartonLabel(customerId: string, transferNo: string) {
|
||||
const response = await http.get<ApiEnvelope<WarehouseTransferCartonLabelDto>, ApiEnvelope<WarehouseTransferCartonLabelDto>>(
|
||||
`/platform/v1/last-leg/warehouse-transfers/${encodeURIComponent(transferNo)}/carton-label`, { params: { customerId } },
|
||||
)
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
export interface WarehouseTransferRelabelBindingDto {
|
||||
operationType: string
|
||||
customerCartonMark: string
|
||||
@@ -61,6 +84,7 @@ export interface WarehouseTransferRelabelBindingDto {
|
||||
export interface WarehouseTransferPalletDto {
|
||||
id: string
|
||||
sequenceNo: number
|
||||
warehouseLocationCode?: string | null
|
||||
palletNo?: string | null
|
||||
cartonCount: number
|
||||
grossWeightKg: number
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import ElementPlus from 'element-plus'
|
||||
import { defineComponent, h } from 'vue'
|
||||
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import WarehouseTransferReceiptPanel from './WarehouseTransferReceiptPanel.vue'
|
||||
import { getWarehouseTransfer, getWarehouseTransferCartonLabel, type WarehouseTransferDetailDto } from '../api/warehouseTransfers'
|
||||
|
||||
vi.mock('../api/warehouseTransfers', () => ({ getWarehouseTransfer: vi.fn(), getWarehouseTransferCartonLabel: vi.fn() }))
|
||||
enableAutoUnmount(afterEach)
|
||||
const DataTableStub = defineComponent({
|
||||
props: ['data', 'columns'],
|
||||
setup(props, { slots }) { return () => h('div', { class: 'test-table' }, [JSON.stringify(props.data), ...(props.data || []).flatMap((row: object) => [slots.boxAction?.({ row }), slots.quantityDifference?.({ row }), slots.boxDifference?.({ row })])]) },
|
||||
})
|
||||
const DialogStub = defineComponent({ props: ['modelValue'], setup(props, { slots }) { return () => props.modelValue ? h('section', { 'data-test': 'box-dialog' }, slots.default?.()) : null } })
|
||||
const render = (transferNo = 'WT-SOURCE') => mount(WarehouseTransferReceiptPanel, { props: { customerId: 'CUSTOMER', transferNo }, global: { plugins: [ElementPlus], stubs: { DataTable: DataTableStub, AppDialog: DialogStub } } })
|
||||
function fixture(): WarehouseTransferDetailDto {
|
||||
return {
|
||||
id: '1', transferNo: 'WT-SOURCE', status: 'WAIT_CUSTOMER_CONFIRMATION', needRelabel: false, packageCount: 4,
|
||||
items: [{ id: '10', skuCode: 'SKU-A', productNameZh: '产品 A', quantity: '120', packageCount: 3 }, { id: '20', skuCode: 'SKU-B', productNameZh: '产品 B', quantity: 30, packageCount: 1 }],
|
||||
cartons: [
|
||||
{ cartonMark: 'BOX-1', cartonIndex: 1, totalCartons: 4, status: 'COUNTED', transferItemId: '10', transferPalletId: '100', quantity: '8', length: 100, width: 20, height: 50, weight: '2.5', countedAt: '2026-10-15T12:00:00' },
|
||||
{ cartonMark: 'BOX-2', cartonIndex: 2, totalCartons: 4, status: 'COUNTED', transferItemId: '10', transferPalletId: '100', quantity: 12, length: 100, width: 20, height: 50, weight: 3.5 },
|
||||
{ cartonMark: 'BOX-3', cartonIndex: 3, totalCartons: 4, status: 'COUNTED', transferItemId: '20', transferPalletId: '200', quantity: 7, length: 100, width: 20, height: 50, weight: 4 },
|
||||
{ cartonMark: 'BOX-MISSING', cartonIndex: 4, totalCartons: 4, status: 'NOT_RECEIVED', transferItemId: '10', quantity: 999, weight: 999 },
|
||||
],
|
||||
pallets: [{ id: '100', sequenceNo: 1, palletNo: 'PALLET-A', cartonCount: 2, grossWeightKg: 6, volumeCbm: .2, status: 'COMPLETED' }, { id: '200', sequenceNo: 2, palletNo: 'PALLET-B', cartonCount: 1, grossWeightKg: 4, volumeCbm: .1, status: 'COMPLETED' }], relabelBindings: [],
|
||||
}
|
||||
}
|
||||
beforeEach(() => { vi.mocked(getWarehouseTransfer).mockReset(); vi.mocked(getWarehouseTransferCartonLabel).mockReset() })
|
||||
afterEach(() => vi.restoreAllMocks())
|
||||
describe('转运预报与实际签收明细', () => {
|
||||
it('keeps declarations separate and totals only counted boxes across pallets', async () => {
|
||||
vi.mocked(getWarehouseTransfer).mockResolvedValue(fixture())
|
||||
const wrapper = render(); await flushPromises()
|
||||
expect(getWarehouseTransfer).toHaveBeenCalledWith('CUSTOMER', 'WT-SOURCE')
|
||||
const summary = wrapper.emitted('summary')!.at(-1)![0]
|
||||
expect(summary).toMatchObject({ forecastBoxes: 4, boxes: 3, quantity: 27, weight: 10 })
|
||||
expect((summary as { volume: number }).volume).toBeCloseTo(.3)
|
||||
const rows = wrapper.findAllComponents(DataTableStub)[0]!.props('data')
|
||||
expect(rows[0]).toMatchObject({ quantity: '120', receivedQuantity: 20, receivedBoxes: 2, quantityDifference: -100, boxDifference: -1 })
|
||||
expect(rows[1]).toMatchObject({ receivedQuantity: 7, quantityDifference: -23, boxDifference: 0 })
|
||||
const tables = wrapper.findAll('.test-table')
|
||||
expect(tables[0]!.text()).toContain('120')
|
||||
expect(tables[1]!.text()).toContain('SKU-A × 20 件'); expect(tables[1]!.text()).toContain('SKU-B × 7 件')
|
||||
expect(tables).toHaveLength(2)
|
||||
await wrapper.findAll('button').find(button => button.text() === '查看箱号明细')!.trigger('click')
|
||||
expect(wrapper.findAll('.test-table')[2]!.text()).not.toContain('BOX-MISSING')
|
||||
await wrapper.findAll('button').find(button => button.text() === '箱内明细')!.trigger('click')
|
||||
const box = wrapper.findAll('[data-test="box-dialog"]')[1]!
|
||||
expect(box.text()).toContain('BOX-1'); expect(box.text()).toContain('产品 A'); expect(box.text()).toContain('"quantity":"8"')
|
||||
expect(box.text()).not.toContain('120')
|
||||
})
|
||||
it('shows zero receipts before signing without treating forecast cartons as actual receipts', async () => {
|
||||
const value = fixture(); value.pallets = []; value.cartons.forEach(box => { box.status = 'EXPECTED' })
|
||||
vi.mocked(getWarehouseTransfer).mockResolvedValue(value)
|
||||
const wrapper = render(); await flushPromises()
|
||||
expect(wrapper.emitted('summary')!.at(-1)![0]).toMatchObject({ boxes: 0, quantity: 0 })
|
||||
expect(wrapper.findAll('.test-table')[0]!.text()).toContain('待签收')
|
||||
expect(wrapper.findAll('.test-table')).toHaveLength(2)
|
||||
expect(wrapper.findAll('button').find(button => button.text() === '查看箱号明细')!.attributes('disabled')).toBeDefined()
|
||||
})
|
||||
it('shows signed positive and zero differences against each forecast row', async () => {
|
||||
const value = fixture(); value.items[0]!.quantity = 10; value.items[0]!.packageCount = 2
|
||||
vi.mocked(getWarehouseTransfer).mockResolvedValue(value)
|
||||
const wrapper = render(); await flushPromises()
|
||||
const table = wrapper.findAll('.test-table')[0]!
|
||||
expect(table.text()).toContain('+10')
|
||||
expect(table.findAll('.receipt-difference').map(cell => cell.text())).toEqual(['+10', '-23'])
|
||||
expect(wrapper.findAllComponents(DataTableStub)[0]!.props('data')[0]).toMatchObject({ quantityDifference: 10, boxDifference: 0 })
|
||||
})
|
||||
it('prints server label HTML with the selected customer and source reference', async () => {
|
||||
vi.mocked(getWarehouseTransfer).mockResolvedValue(fixture())
|
||||
vi.mocked(getWarehouseTransferCartonLabel).mockResolvedValue({ transferNo: 'WT-SOURCE', cartons: fixture().cartons, printableHtml: '<html>SERVER-BOX-LABEL</html>' })
|
||||
const popup = { opener: window, document: { open: vi.fn(), write: vi.fn(), close: vi.fn() }, focus: vi.fn(), print: vi.fn(), close: vi.fn() }
|
||||
vi.spyOn(window, 'open').mockReturnValue(popup as unknown as Window)
|
||||
const wrapper = render(); await flushPromises()
|
||||
await wrapper.findAll('button').find(button => button.text() === '打印箱唛')!.trigger('click'); await flushPromises()
|
||||
expect(getWarehouseTransferCartonLabel).toHaveBeenCalledExactlyOnceWith('CUSTOMER', 'WT-SOURCE')
|
||||
expect(popup.document.write).toHaveBeenCalledWith('<html>SERVER-BOX-LABEL</html>')
|
||||
expect(popup.print).toHaveBeenCalledOnce()
|
||||
expect(popup.opener).toBeNull()
|
||||
})
|
||||
it('closes the print window and leaves receipt details available when labels fail', async () => {
|
||||
vi.mocked(getWarehouseTransfer).mockResolvedValue(fixture())
|
||||
vi.mocked(getWarehouseTransferCartonLabel).mockRejectedValue(new Error('标签暂不可用'))
|
||||
const popup = { close: vi.fn(), print: vi.fn() }
|
||||
vi.spyOn(window, 'open').mockReturnValue(popup as unknown as Window)
|
||||
const wrapper = render(); await flushPromises()
|
||||
await wrapper.findAll('button').find(button => button.text() === '打印箱唛')!.trigger('click'); await flushPromises()
|
||||
expect(popup.close).toHaveBeenCalledOnce(); expect(popup.print).not.toHaveBeenCalled()
|
||||
expect(wrapper.text()).toContain('箱唛加载失败,请重试')
|
||||
expect(wrapper.text()).toContain('签收箱号明细(3 箱)')
|
||||
})
|
||||
it('does not request another order when the source reference is missing', async () => {
|
||||
const wrapper = render(''); await flushPromises()
|
||||
expect(getWarehouseTransfer).not.toHaveBeenCalled()
|
||||
expect(wrapper.text()).toContain('缺少客户或来源转运单号')
|
||||
})
|
||||
it('discards a stale response after switching transfers', async () => {
|
||||
let resolveOld!: (value: WarehouseTransferDetailDto) => void
|
||||
vi.mocked(getWarehouseTransfer).mockImplementationOnce(() => new Promise(resolve => { resolveOld = resolve })).mockResolvedValueOnce({ ...fixture(), transferNo: 'WT-NEW', packageCount: 9 })
|
||||
const wrapper = render(); await wrapper.setProps({ transferNo: 'WT-NEW' }); await flushPromises()
|
||||
resolveOld(fixture()); await flushPromises()
|
||||
expect(wrapper.emitted('summary')!.at(-1)![0]).toMatchObject({ forecastBoxes: 9 })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,143 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { Eye, Printer } from '@lucide/vue'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import { getWarehouseTransfer, getWarehouseTransferCartonLabel, type WarehouseTransferCartonDto, type WarehouseTransferDetailDto } from '../api/warehouseTransfers'
|
||||
import { formatOverseasDateTime } from '../lastMilePresentation'
|
||||
|
||||
const props = defineProps<{ customerId: string; transferNo: string }>()
|
||||
const emit = defineEmits<{ summary: [value: { boxes: number; forecastBoxes: number; forecastSkus: number; skus: number; quantity: number; pallets: number; weight: number; volume: number; warehouseId?: string | null } | null] }>()
|
||||
const detail = ref<WarehouseTransferDetailDto | null>(null)
|
||||
const printing = ref(false)
|
||||
const printError = ref<ApiErrorDetails | null>(null)
|
||||
const loading = ref(false)
|
||||
const error = ref<ApiErrorDetails | null>(null)
|
||||
const selectedBox = ref<WarehouseTransferCartonDto | null>(null)
|
||||
const boxOpen = ref(false)
|
||||
const cartonsOpen = ref(false)
|
||||
let generation = 0
|
||||
const cartons = computed(() => (detail.value?.cartons || []).filter(box => box.status === 'COUNTED'))
|
||||
function itemFor(box: WarehouseTransferCartonDto) { return detail.value?.items.find(item => box.transferItemId != null && String(item.id) === String(box.transferItemId)) }
|
||||
function volume(box: WarehouseTransferCartonDto) { return box.length == null || box.width == null || box.height == null ? null : Number(box.length) * Number(box.width) * Number(box.height) / 1_000_000 }
|
||||
function formatNumber(value: number | null, digits = 0) { return value == null || !Number.isFinite(value) ? '—' : value.toFixed(digits) }
|
||||
const totals = computed(() => ({
|
||||
quantity: cartons.value.reduce((sum, box) => sum + Number(box.quantity || 0), 0),
|
||||
weight: cartons.value.reduce((sum, box) => sum + Number(box.weight || 0), 0),
|
||||
volume: cartons.value.reduce((sum, box) => sum + Number(volume(box) || 0), 0),
|
||||
skus: new Set(cartons.value.map(box => itemFor(box)?.skuCode).filter(Boolean)).size,
|
||||
}))
|
||||
watch([detail, totals], () => emit('summary', detail.value ? { ...totals.value, boxes: cartons.value.length, forecastBoxes: detail.value.packageCount, forecastSkus: new Set(detail.value.items.map(item => item.skuCode).filter(Boolean)).size, pallets: detail.value.pallets.length, warehouseId: detail.value.operatingWarehouseId } : null))
|
||||
const comparisonRows = computed(() => (detail.value?.items || []).map(item => {
|
||||
const boxes = cartons.value.filter(box => box.transferItemId != null && String(box.transferItemId) === String(item.id))
|
||||
const receivedQuantity = boxes.reduce((sum, box) => sum + Number(box.quantity || 0), 0)
|
||||
return { ...item, receivedQuantity, receivedBoxes: boxes.length, quantityDifference: receivedQuantity - Number(item.quantity || 0), boxDifference: boxes.length - Number(item.packageCount || 0) }
|
||||
}))
|
||||
function differenceText(value: unknown) {
|
||||
if (!cartons.value.length) return '待签收'
|
||||
const difference = Number(value)
|
||||
return difference > 0 ? `+${difference}` : String(difference)
|
||||
}
|
||||
const palletRows = computed(() => (detail.value?.pallets || []).map(pallet => {
|
||||
const boxes = cartons.value.filter(box => box.transferPalletId != null && String(box.transferPalletId) === String(pallet.id))
|
||||
const skus = new Map<string, number>()
|
||||
for (const box of boxes) { const sku = itemFor(box)?.skuCode || 'SKU 未留存'; skus.set(sku, (skus.get(sku) || 0) + Number(box.quantity || 0)) }
|
||||
return { ...pallet, receivedBoxes: boxes.length, skuCount: skus.size, quantity: boxes.reduce((sum, box) => sum + Number(box.quantity || 0), 0), skuSummary: [...skus].map(([sku, quantity]) => `${sku} × ${quantity} 件`).join(';') || '—' }
|
||||
}))
|
||||
const cartonRows = computed(() => cartons.value.map(box => ({ ...box, sku: itemFor(box)?.skuCode || '—', palletNo: detail.value?.pallets.find(pallet => box.transferPalletId != null && String(pallet.id) === String(box.transferPalletId))?.palletNo || '未关联托盘', dimensions: [box.length, box.width, box.height].every(value => value != null) ? `${box.length} × ${box.width} × ${box.height}` : '—', volumeText: formatNumber(volume(box), 6) })))
|
||||
const boxItems = computed(() => selectedBox.value ? [{ sku: itemFor(selectedBox.value)?.skuCode || '—', name: itemFor(selectedBox.value)?.productNameZh || '—', quantity: selectedBox.value.quantity ?? '—' }] : [])
|
||||
const receiptLeadingColumns: DataTableColumn[] = [
|
||||
{ key: 'receivedQuantity', label: '实收件数', width: 100, align: 'center', headerAlign: 'center' },
|
||||
{ key: 'receivedBoxes', label: '实收箱数', width: 100, align: 'center', headerAlign: 'center' },
|
||||
]
|
||||
const forecastColumns: DataTableColumn[] = [
|
||||
{ key: 'skuCode', label: 'SKU', width: 500, singleLine: true }, { key: 'productNameZh', label: '中文品名', width: 200, singleLine: false },
|
||||
...receiptLeadingColumns,
|
||||
{ key: 'quantity', label: '预报件数', width: 100, align: 'center', headerAlign: 'center' },
|
||||
{ key: 'packageCount', label: '预计箱数', width: 120, align: 'center', headerAlign: 'center' },
|
||||
{ key: 'quantityDifference', label: '件数差异', slotName: 'quantityDifference', width: 130, align: 'center', headerAlign: 'center' },
|
||||
{ key: 'boxDifference', label: '箱数差异', slotName: 'boxDifference', width: 240, align: 'center', headerAlign: 'center' },
|
||||
]
|
||||
const cartonColumns: DataTableColumn[] = [
|
||||
{ key: 'action', label: '操作', slotName: 'boxAction', width: 140, fixed: 'left' },
|
||||
{ key: 'cartonMark', label: '实收箱唛', minWidth: 220 }, { key: 'palletNo', label: '所属托盘', minWidth: 210 },
|
||||
{ key: 'sku', label: 'SKU', minWidth: 160 }, { key: 'quantity', label: '实收件数', width: 100 },
|
||||
{ key: 'dimensions', label: '长 × 宽 × 高(cm)', minWidth: 150 }, { key: 'weight', label: '重量(kg)', width: 110 }, { key: 'volumeText', label: '体积(m³)', width: 120 },
|
||||
]
|
||||
const palletColumns: DataTableColumn[] = [
|
||||
{ key: 'palletNo', label: '托盘号', width: 500, singleLine: false }, { key: 'warehouseLocationCode', label: '库位', width: 200, singleLine: false },
|
||||
...receiptLeadingColumns.map(column => column.key === 'receivedQuantity' ? { ...column, key: 'quantity' } : column),
|
||||
{ key: 'skuCount', label: 'SKU 种数', width: 100, align: 'center', headerAlign: 'center' }, { key: 'grossWeightKg', label: '总重量(kg)', width: 120, align: 'center', headerAlign: 'center' },
|
||||
{ key: 'volumeCbm', label: '总体积(m³)', width: 130, align: 'center', headerAlign: 'center' }, { key: 'skuSummary', label: 'SKU / 件数汇总', width: 240, singleLine: false },
|
||||
]
|
||||
const boxItemColumns: DataTableColumn[] = [{ key: 'sku', label: 'SKU', minWidth: 180 }, { key: 'name', label: '品名', minWidth: 150 }, { key: 'quantity', label: '实际清点件数', width: 120 }]
|
||||
function showBox(cartonMark: unknown) {
|
||||
const box = cartons.value.find(value => value.cartonMark === cartonMark)
|
||||
if (!box) return
|
||||
selectedBox.value = box; boxOpen.value = true
|
||||
}
|
||||
async function printCartons() {
|
||||
if (printing.value || !detail.value?.cartons.length) return
|
||||
const printWindow = window.open('', '_blank')
|
||||
if (!printWindow) { ElMessage.warning('浏览器阻止了打印窗口,请允许弹窗后重试'); return }
|
||||
printWindow.opener = null
|
||||
printing.value = true; printError.value = null
|
||||
const ticket = generation
|
||||
try {
|
||||
const label = await getWarehouseTransferCartonLabel(props.customerId, props.transferNo)
|
||||
if (ticket !== generation) { printWindow.close(); return }
|
||||
if (!label.cartons.length) { printWindow.close(); ElMessage.warning('暂无可打印的箱唛'); return }
|
||||
printWindow.document.open(); printWindow.document.write(label.printableHtml); printWindow.document.close()
|
||||
printWindow.focus(); printWindow.print()
|
||||
} catch (cause) {
|
||||
printWindow.close()
|
||||
if (ticket === generation) printError.value = getApiErrorDetails(cause, '箱唛加载失败,请重试')
|
||||
} finally { printing.value = false }
|
||||
}
|
||||
async function load() {
|
||||
const ticket = ++generation
|
||||
detail.value = null; selectedBox.value = null; boxOpen.value = false; cartonsOpen.value = false; error.value = null; printError.value = null
|
||||
if (!props.customerId || !props.transferNo) { loading.value = false; error.value = { message: '缺少客户或来源转运单号,无法查询签收明细' }; return }
|
||||
loading.value = true
|
||||
try { const value = await getWarehouseTransfer(props.customerId, props.transferNo); if (ticket === generation) detail.value = value }
|
||||
catch (cause) { if (ticket === generation) error.value = getApiErrorDetails(cause, '转运预报与签收明细加载失败') }
|
||||
finally { if (ticket === generation) loading.value = false }
|
||||
}
|
||||
watch(() => [props.customerId, props.transferNo], load, { immediate: true })
|
||||
onBeforeUnmount(() => { generation++ })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section v-loading="loading" class="transfer-receipt-panel" aria-label="仓储转运预报与实际签收">
|
||||
<LoadErrorState v-if="error" title="签收明细加载失败" :message="error.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
|
||||
<template v-else-if="detail">
|
||||
<h3>预报与实际签收对比 <small>差异 = 实收 − 预报{{ cartons.length ? '(按当前已清点箱汇总)' : ',待签收后显示' }}</small></h3>
|
||||
<DataTable class="receipt-aligned-table" :fit="false" :data="comparisonRows" :columns="forecastColumns" row-key="id" :show-pagination="false" empty-text="暂无预报货物明细">
|
||||
<template #quantityDifference="{ row }"><span :class="{ 'receipt-difference': cartons.length && Number(row.quantityDifference) !== 0 }">{{ differenceText(row.quantityDifference) }}</span></template>
|
||||
<template #boxDifference="{ row }"><span :class="{ 'receipt-difference': cartons.length && Number(row.boxDifference) !== 0 }">{{ differenceText(row.boxDifference) }}</span></template>
|
||||
</DataTable>
|
||||
<h3>托盘签收汇总</h3><DataTable class="receipt-aligned-table" :fit="false" :data="palletRows" :columns="palletColumns" row-key="id" :show-pagination="false" empty-text="尚未创建托盘" />
|
||||
<div class="receipt-box-heading"><h3>签收箱号明细({{ cartons.length }} 箱)</h3><el-button type="primary" size="small" :disabled="!cartons.length" @click="cartonsOpen = true">查看箱号明细</el-button><el-button v-if="detail.status !== 'DRAFT'" type="primary" size="small" :loading="printing" :disabled="!detail.cartons.length" @click="printCartons"><Printer :size="14" />打印箱唛</el-button></div>
|
||||
<LoadErrorState v-if="printError" title="箱唛打印失败" :message="printError.message" :loading="printing" @retry="printCartons"><template #details><ApiErrorMeta :details="printError" /></template></LoadErrorState>
|
||||
<AppDialog v-model="cartonsOpen" class="admin-density-dialog" title="签收箱号明细" width="1120px" :show-footer="false"><DataTable :data="cartonRows" :columns="cartonColumns" row-key="cartonMark" :show-pagination="false" empty-text="暂无实际签收箱,等待 PDA 清点">
|
||||
<template #boxAction="{ row }"><el-button link type="primary" @click="showBox(row.cartonMark)"><Eye :size="14" />箱内明细</el-button></template>
|
||||
</DataTable></AppDialog>
|
||||
</template>
|
||||
<AppDialog class="admin-density-dialog" v-model="boxOpen" title="箱内清点明细" width="820px" :show-footer="false">
|
||||
<template v-if="selectedBox"><p>{{ selectedBox.cartonMark }} · 清点时间 {{ formatOverseasDateTime(selectedBox.countedAt) }}</p><DataTable :data="boxItems" :columns="boxItemColumns" row-key="sku" :show-pagination="false" /><p v-if="selectedBox.discrepancyReason">差异说明:{{ selectedBox.discrepancyReason }}</p></template>
|
||||
</AppDialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.receipt-box-heading{display:flex;align-items:center;justify-content:flex-start;flex-wrap:wrap;gap:12px}
|
||||
.transfer-receipt-panel{display:grid;gap:var(--admin-space-2)}
|
||||
.receipt-aligned-table{width:100%;max-width:1490px;min-width:0}
|
||||
.receipt-difference{color:var(--el-color-danger);font-weight:600}
|
||||
h3 small{margin-left:8px;color:var(--cc-muted);font-size:var(--admin-font-caption);font-weight:400}
|
||||
h3{margin:0;font-size:var(--admin-font-section)}
|
||||
</style>
|
||||
@@ -14,9 +14,9 @@ import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
||||
import { overseasWarehousePermissions as permission } from '../permissions'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
const access = useAccessPreviewStore(); const rows = ref<CargoDiscardSummary[]>([]); const customers = ref<PlatformTenantResource[]>([]); const page = ref(1); const pageSize = ref(10); const total = ref(0); const loading = ref(false); const detailOpen = ref(false); const detail = ref<CargoDiscardDetail | null>(null); const filters = ref<QueryModel>({ customerId: '', discardNo: '', inboundCode: '', status: '' })
|
||||
const fields: QueryField[] = [{ key: 'customerId', type: 'select', label: '客户', placeholder: '请选择客户', options: [] }, { key: 'discardNo', type: 'input', label: '废弃单号', placeholder: '废弃单号', width: 220 }, { key: 'inboundCode', type: 'input', label: '入仓码', placeholder: '请输入入仓码', width: 180 }, { key: 'status', type: 'select', label: '状态', placeholder: '全部状态', options: [{ label: '待审核', value: 'PENDING_REVIEW' }, { label: '已审核', value: 'APPROVED' }, { label: 'PDA处理中', value: 'PDA_COMPLETED' }, { label: '已完成', value: 'COMPLETED' }, { label: '已驳回', value: 'REJECTED' }, { label: '已取消', value: 'CANCELLED' }] }]
|
||||
const fields: QueryField[] = [{ key: 'customerId', type: 'select', label: '客户', placeholder: '请选择客户', options: [] }, { key: 'discardNo', type: 'input', label: '废弃单号', placeholder: '废弃单号', width: 220 }, { key: 'inboundCode', type: 'input', label: '入仓码', placeholder: '请输入入仓码', width: 180 }, { key: 'status', type: 'select', label: '状态', placeholder: '全部状态', options: [{ label: '待审核', value: 'PENDING_REVIEW' }, { label: '已审核', value: 'APPROVED' }, { label: '待客户确认', value: 'PDA_COMPLETED' }, { label: '已完成', value: 'COMPLETED' }, { label: '已驳回', value: 'REJECTED' }, { label: '已取消', value: 'CANCELLED' }] }]
|
||||
const columns: DataTableColumn[] = [{ key: 'actions', label: '操作', width: 140, fixed: 'left', slotName: 'actions' }, { key: 'discardNo', label: '废弃单号', minWidth: 180 }, { key: 'inboundCodes', label: '入仓码', minWidth: 180 }, { key: 'overseasWarehouseName', label: '仓库', minWidth: 140 }, { key: 'totalPackages', label: '总箱数', width: 90 }, { key: 'totalFee', label: '废弃费用', width: 120, formatter: (row: CargoDiscardSummary) => `${row.currency || 'USD'} ${row.totalFee ?? 0}` }, { key: 'createdBy', label: '申请人', width: 110 }, { key: 'createdAt', label: '创建时间', minWidth: 170 }, { key: 'status', label: '状态', width: 150, slotName: 'status' }]
|
||||
const statusLabels: Record<string, string> = { PENDING_REVIEW: '待审核', APPROVED: '已审核,待 PDA', PDA_COMPLETED: 'PDA 已完成,待仓库确认', COMPLETED: '已完成', REJECTED: '已驳回', CANCELLED: '已取消' }
|
||||
const statusLabels: Record<string, string> = { PENDING_REVIEW: '待审核', APPROVED: '已审核,待 PDA', PDA_COMPLETED: 'PDA 已完成,待客户确认', COMPLETED: '已完成', REJECTED: '已驳回', CANCELLED: '已取消' }
|
||||
const photoUrls = reactive<Record<string, string>>({}); const photoLoading = reactive<Record<string, boolean>>({}); const photoErrors = reactive<Record<string, boolean>>({})
|
||||
const photoPreviewUrls = computed(() => (detail.value?.photos || []).map(photo => photoUrls[photo.fileId]).filter(Boolean))
|
||||
let photoGeneration = 0
|
||||
|
||||
@@ -3,9 +3,11 @@ import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { getLastMileOrder } from '../api/lastMileOrders'
|
||||
import LastMileOrderDetailView from './LastMileOrderDetailView.vue'
|
||||
import { getPlatformWarehouse } from '@/modules/warehouses'
|
||||
import { getWarehouseTransfer } from '../api/warehouseTransfers'
|
||||
|
||||
const push = vi.hoisted(() => vi.fn())
|
||||
vi.mock('@/modules/warehouses', () => ({ getPlatformWarehouse: vi.fn() }))
|
||||
vi.mock('vue-router', () => ({ useRoute: () => ({ params: { orderId: 'internal-order-id' } }), useRouter: () => ({ push }) }))
|
||||
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => false }) }))
|
||||
vi.mock('../api/lastMileOrders', () => ({ getLastMileOrder: vi.fn(), createLastMileOrderFee: vi.fn() }))
|
||||
@@ -63,9 +65,34 @@ describe('LastMileOrderDetailView domestic detail alignment', () => {
|
||||
expect(wrapper.get('[aria-label="本票转运与换标履约"]').text()).toContain('不得提前出库')
|
||||
})
|
||||
|
||||
it('fills the receiving warehouse from its ID when the order name is absent', async () => {
|
||||
vi.mocked(getLastMileOrder).mockResolvedValue({ order: { id: 'tail', customerId: 'C1', orderNo: 'WT-TAIL', orderType: 'WAREHOUSE_TRANSFER', overseasWarehouseId: '193000000000200003' }, cargo: [], fees: [], statusHistory: [], operationHistory: [] })
|
||||
vi.mocked(getPlatformWarehouse).mockResolvedValue({ warehouse: { id: '193000000000200003', warehouseName: '洛杉矶收货仓', warehouseCode: 'US-LAX-01', tenantId: '0', internalCode: null, warehouseType: 'OVERSEAS', countryCode: 'US', timezone: 'America/Los_Angeles', contactName: null, contactPhone: null, contactEmail: null, status: 'ACTIVE', remark: null }, addresses: [], capabilities: [] })
|
||||
const wrapper = mount(LastMileOrderDetailView, { global: { plugins: [ElementPlus] } }); await flushPromises()
|
||||
expect(getPlatformWarehouse).toHaveBeenCalledWith('193000000000200003')
|
||||
expect(wrapper.get('[aria-label="尾程订单概览"]').text()).toContain('海外仓洛杉矶收货仓')
|
||||
expect(wrapper.get('[aria-label="基本信息"]').text()).toContain('海外仓洛杉矶收货仓')
|
||||
})
|
||||
|
||||
it('loads transfer declarations and actual cartons on the cargo tab using the source transfer', async () => {
|
||||
vi.mocked(getLastMileOrder).mockResolvedValue({ order: { id: 'tail', customerId: 'customer-1', orderNo: 'TAIL-NO', sourceOrderNo: 'WT-SOURCE', orderType: 'WAREHOUSE_TRANSFER' }, cargo: [], fees: [], statusHistory: [], operationHistory: [] })
|
||||
vi.mocked(getWarehouseTransfer).mockResolvedValue({ id: 'transfer', transferNo: 'WT-SOURCE', status: 'WAIT_PDA_RECEIVING', needRelabel: false, packageCount: 5, items: [], cartons: [], pallets: [], relabelBindings: [] })
|
||||
const wrapper = mount(LastMileOrderDetailView, { global: { plugins: [ElementPlus] } }); await flushPromises()
|
||||
expect(getWarehouseTransfer).not.toHaveBeenCalled()
|
||||
await wrapper.get('#tab-cargo').trigger('click'); await flushPromises()
|
||||
expect(getWarehouseTransfer).toHaveBeenCalledExactlyOnceWith('customer-1', 'WT-SOURCE')
|
||||
const panel = wrapper.get('[aria-label="仓储转运预报与实际签收"]')
|
||||
expect(wrapper.get('[aria-label="尾程订单概览"]').text()).toContain('预报箱数5 箱')
|
||||
expect(wrapper.get('[aria-label="尾程订单概览"]').text()).toContain('实收箱数0 箱')
|
||||
expect(panel.text()).toContain('预报与实际签收对比')
|
||||
expect(panel.text()).toContain('签收箱号明细(0 箱)')
|
||||
expect(panel.text()).toContain('查看箱号明细')
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
push.mockReset()
|
||||
vi.mocked(getWarehouseTransfer).mockReset()
|
||||
vi.mocked(getPlatformWarehouse).mockReset()
|
||||
vi.mocked(getLastMileOrder).mockResolvedValue({
|
||||
order: { id: 'internal-order-id', customerId: 'customer-1', orderNo: 'FL-WH-001', currentStatus: 'PENDING_ASSIGNMENT', customerName: '示例客户', overseasWarehouseName: '洛杉矶仓', packageCount: 0, palletCount: 2, grossWeightKg: 30, netWeightKg: 22, volumeCbm: 6.374, inboundCode: 'IN-001', containerNo: 'CONT-001', customerRemark: '完整保留客户备注', updatedAt: '2026-09-14T10:00:00' },
|
||||
cargo: [{ id: 'cargo-1', skuCode: 'SKU-001', productNameZh: '测试货物' }], fees: [], statusHistory: [], operationHistory: [],
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import '@/styles/adminDensity.css'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { Plus } from '@lucide/vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { getPlatformWarehouse } from '@/modules/warehouses'
|
||||
import { listSuppliers, type Supplier } from '@/modules/suppliers'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
@@ -19,6 +20,7 @@ import OverseasDetailShell from '../components/OverseasDetailShell.vue'
|
||||
import { businessOrderLink } from '../businessOrderLink'
|
||||
import OrderReferenceText from '../components/OrderReferenceText.vue'
|
||||
import WarehouseTransferFulfillmentPanel from '../components/WarehouseTransferFulfillmentPanel.vue'
|
||||
import WarehouseTransferReceiptPanel from '../components/WarehouseTransferReceiptPanel.vue'
|
||||
|
||||
interface DetailField { label: string; value: string | number; wide?: boolean }
|
||||
interface CargoRow { id: string; sku: string; productName: string; pallets: number; cartons: number; quantity: number; weight: number; volume: number }
|
||||
@@ -31,6 +33,20 @@ const router = useRouter()
|
||||
const access = useAccessPreviewStore()
|
||||
const orderId = computed(() => String(route.params.orderId || ''))
|
||||
const detail = ref<LastMileOrderDetailDto | null>(null)
|
||||
const receiptSummary = ref<{ boxes: number; forecastBoxes: number; forecastSkus: number; skus: number; quantity: number; pallets: number; weight: number; volume: number; warehouseId?: string | null } | null>(null)
|
||||
const resolvedWarehouseName = ref('')
|
||||
const warehouseLookupFailed = ref(false)
|
||||
const warehouseDisplayName = computed(() => detail.value?.order.overseasWarehouseName || resolvedWarehouseName.value || detail.value?.order.overseasWarehouseCode || (warehouseLookupFailed.value ? '仓库名称加载失败' : '—'))
|
||||
watch(() => [detail.value?.order.overseasWarehouseId || receiptSummary.value?.warehouseId, detail.value?.order.overseasWarehouseName] as const, async ([warehouseId, warehouseName], _, onCleanup) => {
|
||||
let current = true
|
||||
onCleanup(() => { current = false })
|
||||
resolvedWarehouseName.value = ''; warehouseLookupFailed.value = false
|
||||
if (!warehouseId || warehouseName) return
|
||||
try {
|
||||
const result = await getPlatformWarehouse(String(warehouseId))
|
||||
if (current) resolvedWarehouseName.value = result.warehouse.warehouseName || result.warehouse.warehouseCode
|
||||
} catch { if (current) warehouseLookupFailed.value = true }
|
||||
})
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const loadError = ref<ApiErrorDetails | null>(null)
|
||||
@@ -76,7 +92,7 @@ const orderFields = computed<DetailField[]>(() => {
|
||||
{ label: '履约类型', value: text(order?.orderTypeName || lastMileOrderTypeLabel(order?.orderType)) },
|
||||
{ label: '入仓码', value: text(order?.inboundCode) },
|
||||
{ label: '货主', value: text(order?.customerName) },
|
||||
{ label: '海外仓', value: text(order?.overseasWarehouseName) },
|
||||
{ label: '海外仓', value: warehouseDisplayName.value },
|
||||
{ label: '转仓目的仓', value: text(order?.transferWarehouseName) },
|
||||
{ label: '目的仓', value: text(order?.fbaWarehouseCode || order?.transferWarehouseName) },
|
||||
{ label: '是否暂扣', value: yesNo(order?.detained) },
|
||||
@@ -115,9 +131,25 @@ const overviewFields = computed(() => {
|
||||
const order = detail.value?.order
|
||||
const warehouseTransfer = order?.orderType === 'WAREHOUSE_TRANSFER'
|
||||
const hasReceivedPackages = warehouseTransfer && order?.receivedPackageCount != null
|
||||
if (warehouseTransfer && receiptSummary.value) {
|
||||
const summary = receiptSummary.value
|
||||
return [
|
||||
{ label: '客户', value: text(order?.customerName) },
|
||||
{ label: '海外仓', value: warehouseDisplayName.value },
|
||||
{ label: '预报箱数', value: `${summary.forecastBoxes} 箱` },
|
||||
{ label: '预报 SKU', value: `${summary.forecastSkus} 种` },
|
||||
{ label: '实收箱数', value: `${summary.boxes} 箱` },
|
||||
{ label: '实收 SKU', value: `${summary.skus} 种` },
|
||||
{ label: '实收件数', value: `${summary.quantity} 件` },
|
||||
{ label: '总托数', value: `${summary.pallets} 托` },
|
||||
{ label: '总重量', value: `${summary.weight.toFixed(3)} kg` },
|
||||
{ label: '总体积', value: `${summary.volume.toFixed(6)} m³` },
|
||||
{ label: '最近更新', value: formatOverseasDateTime(order?.updatedAt) },
|
||||
]
|
||||
}
|
||||
return [
|
||||
{ label: '客户', value: text(order?.customerName) },
|
||||
{ label: '海外仓', value: text(order?.overseasWarehouseName) },
|
||||
{ label: '海外仓', value: warehouseDisplayName.value },
|
||||
...(isReturnOrder.value ? [{ label: showReturnForecast.value ? '预报总件数' : '实收总件数', value: `${showReturnForecast.value ? detail.value?.returnReceipt?.order.expectedQuantity ?? order?.productQuantity ?? 0 : detail.value?.returnReceipt?.order.receivedQuantity ?? order?.productQuantity ?? 0} 件` }]
|
||||
: [{ label: warehouseTransfer ? (hasReceivedPackages ? '实收总箱数' : '客户预报总箱数') : '总箱数', value: (hasReceivedPackages ? order?.receivedPackageCount : order?.packageCount) == null ? '—' : `${hasReceivedPackages ? order?.receivedPackageCount : order?.packageCount} 箱` }]),
|
||||
{ label: '总托数', value: order?.palletCount == null ? '—' : order.palletCount + ' 托' },
|
||||
@@ -248,7 +280,7 @@ onMounted(load)
|
||||
<div v-for="field in basicFields" :key="field.label" :class="{ wide: field.wide }"><dt>{{ field.label }}</dt><dd :class="{ 'detail-field-value--business': isSingleLineBusinessField(field), 'package-difference--nonzero': isNonzeroDifference(field) }" :title="isSingleLineBusinessField(field) ? String(field.value) : undefined">{{ field.value }}</dd></div>
|
||||
</dl>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane name="cargo"><template #label><span class="density-tab-icon" aria-hidden="true">📦</span><span>货物信息</span></template><section class="table-panel"><DataTable :data="cargo" :columns="cargoColumns" column-settings-mode="hide" row-key="id" :empty-text="isReturnOrder ? '暂无客户预报货物明细' : '暂无货物明细'"></DataTable></section></el-tab-pane>
|
||||
<el-tab-pane name="cargo"><template #label><span class="density-tab-icon" aria-hidden="true">📦</span><span>货物信息</span></template><section class="table-panel"><WarehouseTransferReceiptPanel v-if="detail?.order.orderType === 'WAREHOUSE_TRANSFER' && activeTab === 'cargo'" :customer-id="String(detail.order.customerId || '')" :transfer-no="String(detail.order.sourceOrderNo || '')" @summary="receiptSummary = $event" /><DataTable v-else-if="detail?.order.orderType !== 'WAREHOUSE_TRANSFER'" :data="cargo" :columns="cargoColumns" column-settings-mode="hide" row-key="id" :empty-text="isReturnOrder ? '暂无客户预报货物明细' : '暂无货物明细'"></DataTable></section></el-tab-pane>
|
||||
<el-tab-pane v-if="detail?.order.orderType === 'RETURN_RELABEL'" name="return-receipt"><template #label><span class="density-tab-icon" aria-hidden="true">📦</span><span>退货签收与箱内清点</span></template>
|
||||
<section v-if="detail.returnReceipt" class="table-panel" aria-label="退货签收与箱内清点">
|
||||
<h3>{{ detail.returnReceipt.order.returnNo }} · 实收 {{ detail.returnReceipt.order.receivedQuantity }} / 预报 {{ detail.returnReceipt.order.expectedQuantity }} 件</h3>
|
||||
|
||||
@@ -44,7 +44,7 @@ const warehouseLabel = computed(() => {
|
||||
return outboundWarehouseId && delivery?.overseasWarehouseId === outboundWarehouseId ? show(delivery.overseasWarehouseName) : '—'
|
||||
})
|
||||
function statusLabel(value: unknown) { const code = String(value || ''); return statusLabels[code] || show(code) }
|
||||
function outboundTypeLabel(value?: string | null) { return value === 'DISCARD_OUTBOUND' ? '废弃出库' : value === 'WAREHOUSE_TRANSFER_OUTBOUND' ? '仓储转运出库' : value === 'ONE_CLICK_DROPSHIP' ? '一件代发出库' : value === 'RETURN_OUTBOUND' ? '退货出库' : value === 'DISPATCH_DELIVERY' ? '派送出库' : show(value) }
|
||||
function outboundTypeLabel(value?: string | null) { return value === 'RELABEL_OUTBOUND' ? '贴标出库' : value === 'DISCARD_OUTBOUND' ? '废弃出库' : value === 'WAREHOUSE_TRANSFER_OUTBOUND' ? '仓储转运出库' : value === 'ONE_CLICK_DROPSHIP' ? '一件代发出库' : value === 'RETURN_OUTBOUND' ? '退货出库' : value === 'DISPATCH_DELIVERY' ? '派送出库' : show(value) }
|
||||
const canCancel = computed(() => access.can(permission.outboundCancel) && detail.value?.outbound.outboundType !== 'DISCARD_OUTBOUND' && ['WAIT_PICK', 'WAIT_OUTBOUND'].includes(String(detail.value?.outbound.status || '')))
|
||||
async function load() { if (!outboundNo.value) { loadError.value = { message: '详情缺少出库单号,请从出库单列表重新进入' }; return } loading.value = true; loadError.value = null; try { detail.value = await getOutbound(outboundNo.value) } catch (cause) { detail.value = null; loadError.value = getApiErrorDetails(cause, '出库单详情加载失败,请重试') } finally { loading.value = false } }
|
||||
async function cancel() { if (!canCancel.value || cancelling.value) return; let reason = ''; try { const result = await ElMessageBox.prompt(`确认取消出库单 ${outboundNo.value}?`, '取消出库单', { customClass: 'admin-density-confirm', inputType: 'textarea', inputPlaceholder: '请输入取消原因', inputPattern: /\S+/, inputErrorMessage: '请输入取消原因', confirmButtonText: '确认取消', cancelButtonText: '返回', type: 'warning' }); reason = result.value.trim() } catch { return } cancelling.value = true; mutationError.value = null; try { await cancelOutbound(outboundNo.value, reason); ElMessage.success('出库单已取消'); await load() } catch (cause) { mutationError.value = getApiErrorDetails(cause, '取消出库单失败,请重试') } finally { cancelling.value = false } }
|
||||
|
||||
@@ -95,10 +95,10 @@ const somePageSelected = computed(() => selectedPageCount.value > 0 && !allPageS
|
||||
const selectedDropshipCustomerCount = computed(() => new Set(dropshipCandidates.value.filter(item => selectedDropshipKeys.value.has(dropshipCandidateKey(item))).map(item => item.customerId).filter(Boolean)).size)
|
||||
const oneClickMarks = computed(() => typeof cartonMarks.value === 'string' ? cartonMarks.value.split(/[,\n,]+/).map(value => value.trim()).filter(Boolean) : [])
|
||||
const columns: DataTableColumn[] = [{ key: 'actions', label: '操作', width: 180, fixed: 'left', slotName: 'actions' }, { key: 'outboundNo', prop: 'outboundNo', label: '出库单号', minWidth: 180 }, { key: 'outboundType', prop: 'outboundType', label: '出库类型', width: 150, slotName: 'outboundType' }, { key: 'lxCodes', prop: 'lxCodes', label: 'LX代码', minWidth: 180 }, { key: 'dispatchNo', prop: 'dispatchNo', label: '关联排车单', minWidth: 160 }, { key: 'status', prop: 'status', label: '状态', width: 130, slotName: 'status' }, { key: 'palletCount', prop: 'palletCount', label: '托盘数', width: 100 }, { key: 'packageCount', prop: 'packageCount', label: '箱数', width: 100 }, { key: 'grossWeightKg', prop: 'grossWeightKg', label: '毛重(kg)', width: 120, align: 'right' }, { key: 'volumeWeightKg', prop: 'volumeWeightKg', label: '材积重(kg)', width: 130, align: 'right' }, { key: 'volumeCbm', prop: 'volumeCbm', label: '体积(m³)', width: 120, align: 'right' }, { key: 'createdAt', prop: 'createdAt', label: '创建时间', minWidth: 180 }]
|
||||
const fields: QueryField[] = [{ key: 'documentNo', type: 'input', label: '出库单号', placeholder: '请输入出库单号', width: 220 }, { key: 'lxCode', type: 'input', label: 'LX代码', placeholder: '请输入领星入库单代码', width: 220 }, { key: 'warehouseId', type: 'input', label: '目的仓', placeholder: '请输入目的仓', width: 220 }, { key: 'status', type: 'select', label: '状态', placeholder: '全部状态', width: 160, options: [{ label: '全部', value: '' }, { label: '待拣货', value: 'WAIT_PICK' }, { label: '拣货中', value: 'PICKING' }, { label: '已拣货', value: 'PICKED' }, { label: '待出库', value: 'WAIT_OUTBOUND' }, { label: '已出库', value: 'OUTBOUNDED' }, { label: '已取消', value: 'CANCELLED' }] }, { key: 'outboundType', type: 'select', label: '出库类型', placeholder: '全部出库类型', width: 170, options: [{ label: '全部', value: '' }, { label: '仓储转运出库', value: 'WAREHOUSE_TRANSFER_OUTBOUND' }, { label: '一件代发出库', value: 'ONE_CLICK_DROPSHIP' }, { label: '退货出库', value: 'RETURN_OUTBOUND' }, { label: '派送出库', value: 'DISPATCH_DELIVERY' }, { label: '废弃出库', value: 'DISCARD_OUTBOUND' }] }]
|
||||
const fields: QueryField[] = [{ key: 'documentNo', type: 'input', label: '出库单号', placeholder: '请输入出库单号', width: 220 }, { key: 'lxCode', type: 'input', label: 'LX代码', placeholder: '请输入领星入库单代码', width: 220 }, { key: 'warehouseId', type: 'input', label: '目的仓', placeholder: '请输入目的仓', width: 220 }, { key: 'status', type: 'select', label: '状态', placeholder: '全部状态', width: 160, options: [{ label: '全部', value: '' }, { label: '待拣货', value: 'WAIT_PICK' }, { label: '拣货中', value: 'PICKING' }, { label: '已拣货', value: 'PICKED' }, { label: '待出库', value: 'WAIT_OUTBOUND' }, { label: '已出库', value: 'OUTBOUNDED' }, { label: '已取消', value: 'CANCELLED' }] }, { key: 'outboundType', type: 'select', label: '出库类型', placeholder: '全部出库类型', width: 170, options: [{ label: '全部', value: '' }, { label: '仓储转运出库', value: 'WAREHOUSE_TRANSFER_OUTBOUND' }, { label: '一件代发出库', value: 'ONE_CLICK_DROPSHIP' }, { label: '退货出库', value: 'RETURN_OUTBOUND' }, { label: '派送出库', value: 'DISPATCH_DELIVERY' }, { label: '废弃出库', value: 'DISCARD_OUTBOUND' }, { label: '贴标出库', value: 'RELABEL_OUTBOUND' }] }]
|
||||
|
||||
async function load(model: QueryModel = filters.value) { Object.assign(filters.value, model); loading.value = true; error.value = null; try { const result = await listOutbounds({ page: page.value, size: pageSize.value, documentNo: String(filters.value.documentNo || '').trim() || undefined, lxCode: String(filters.value.lxCode || '').trim() || undefined, warehouseId: String(filters.value.warehouseId || '').trim() || undefined, status: String(filters.value.status || '').trim() || undefined, outboundType: String(filters.value.outboundType || '').trim() || undefined }); rows.value = result.records.map((row) => ({ ...row, createdAt: formatOverseasDateTime(row.createdAt) })); total.value = result.total } catch (cause) { error.value = getApiErrorDetails(cause, '出库单列表加载失败') } finally { loading.value = false } }
|
||||
function outboundTypeLabel(value?: string | null) { return value === 'DISCARD_OUTBOUND' ? '废弃出库' : value === 'WAREHOUSE_TRANSFER_OUTBOUND' ? '仓储转运出库' : value === 'ONE_CLICK_DROPSHIP' ? '一件代发出库' : value === 'RETURN_OUTBOUND' ? '退货出库' : value === 'DISPATCH_DELIVERY' ? '派送出库' : value || '—' }
|
||||
function outboundTypeLabel(value?: string | null) { return value === 'RELABEL_OUTBOUND' ? '贴标出库' : value === 'DISCARD_OUTBOUND' ? '废弃出库' : value === 'WAREHOUSE_TRANSFER_OUTBOUND' ? '仓储转运出库' : value === 'ONE_CLICK_DROPSHIP' ? '一件代发出库' : value === 'RETURN_OUTBOUND' ? '退货出库' : value === 'DISPATCH_DELIVERY' ? '派送出库' : value || '—' }
|
||||
async function loadCustomerOptions() {
|
||||
const records: PlatformTenantResource[] = []
|
||||
let page = 1
|
||||
|
||||
Reference in New Issue
Block a user