Release/test 合并上生产 #2
@@ -1,6 +1,6 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { overseasHttp } from '@/shared/api/overseasHttp'
|
||||
import { cancelDispatch, createDispatchDraftContainer, deleteDispatchDraftContainer, getDispatch, listDispatchCandidates, listDispatchDrafts, listDispatches, lockDispatchDraftPallet, preArrangeDispatch, releaseDispatchDraft, transferDispatchToDelivery, unlockDispatchDraftPallet, updateDispatch } from './dispatches'
|
||||
import { cancelDispatch, createDispatchDraftContainer, deleteDispatchDraftContainer, getDispatch, getDispatchBolPdf, listDispatchCandidates, listDispatchDrafts, listDispatches, lockDispatchDraftPallet, preArrangeDispatch, releaseDispatchDraft, transferDispatchToDelivery, unlockDispatchDraftPallet, updateDispatch } from './dispatches'
|
||||
|
||||
vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn(), post: vi.fn(), put: vi.fn(), delete: vi.fn() } }))
|
||||
|
||||
@@ -38,6 +38,12 @@ describe('dispatch overseas API', () => {
|
||||
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/PC%2F1')
|
||||
})
|
||||
|
||||
it('requests the generated BOL as a PDF blob', async () => {
|
||||
vi.mocked(overseasHttp.get).mockResolvedValue(new Blob(['%PDF-test']))
|
||||
await getDispatchBolPdf('PC/1')
|
||||
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/PC%2F1/prints/bol', { responseType: 'blob' })
|
||||
})
|
||||
|
||||
it('loads candidates and creates a pre-arrange dispatch in an explicit customer scope', async () => {
|
||||
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', message: 'success', data: [] })
|
||||
vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', message: 'success', data: { id: '1', dispatchNo: 'PC-1' } })
|
||||
|
||||
@@ -145,6 +145,13 @@ export async function getDispatch(dispatchNo: string) {
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
export async function getDispatchBolPdf(dispatchNo: string) {
|
||||
return overseasHttp.get<Blob, Blob>(
|
||||
`/platform/v1/last-leg/dispatches/${encodeURIComponent(dispatchNo)}/prints/bol`,
|
||||
{ responseType: 'blob' },
|
||||
)
|
||||
}
|
||||
|
||||
export async function updateDispatch(dispatchNo: string, payload: UpdateDispatchPayload) {
|
||||
const response = await overseasHttp.put<ApiEnvelope<DispatchSummaryDto>, ApiEnvelope<DispatchSummaryDto>>(
|
||||
`/platform/v1/last-leg/dispatches/${encodeURIComponent(dispatchNo)}`, payload,
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import DispatchBolPrintDialog from './DispatchBolPrintDialog.vue'
|
||||
|
||||
afterEach(() => {
|
||||
document.body.classList.remove('printing-dispatch-bol')
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('DispatchBolPrintDialog', () => {
|
||||
it('groups print sheets by FBA warehouse and totals each A4 sheet', () => {
|
||||
const wrapper = mount(DispatchBolPrintDialog, {
|
||||
props: {
|
||||
modelValue: true,
|
||||
detail: {
|
||||
id: '1', dispatchNo: 'DSP-1', overseasWarehouseName: 'NOBLE LOGISTICS INC', carrierName: 'Harbor',
|
||||
bolLines: [
|
||||
{ containerNo: 'D2144', palletNo: 'D2144-112-1', fbaWarehouseCode: 'LBE1', warehouseNo: 'NPHK1', fbaShipmentId: 'FBA1', poReferenceNo: 'PO1', packageCount: 138 },
|
||||
{ containerNo: 'D2144', palletNo: 'D2144-112-2', fbaWarehouseCode: 'LBE1', warehouseNo: 'NPHK2', fbaShipmentId: 'FBA2', poReferenceNo: 'PO2', packageCount: 2 },
|
||||
{ containerNo: 'D2145', palletNo: 'D2145-113-1', fbaWarehouseCode: 'ONT8', warehouseNo: 'NPHK3', packageCount: 5 },
|
||||
],
|
||||
},
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
ElDialog: { template: '<div><slot/><slot name="footer"/></div>' },
|
||||
ElButton: { template: '<button @click="$emit(\'click\')"><slot/></button>' },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const sheets = wrapper.findAll('.dispatch-bol-sheet')
|
||||
expect(sheets).toHaveLength(2)
|
||||
expect(sheets[0]!.text()).toContain('LBE1')
|
||||
expect(sheets[0]!.find('.bol-total').text()).toContain('140')
|
||||
expect(sheets[1]!.find('.bol-total').text()).toContain('5')
|
||||
})
|
||||
|
||||
it('opens the native browser print flow', async () => {
|
||||
vi.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => { callback(0); return 1 })
|
||||
const print = vi.spyOn(window, 'print').mockImplementation(() => {})
|
||||
const wrapper = mount(DispatchBolPrintDialog, {
|
||||
props: { modelValue: true, detail: { id: '1', dispatchNo: 'DSP-1' } },
|
||||
global: { stubs: { ElDialog: { template: '<div><slot/><slot name="footer"/></div>' }, ElButton: { template: '<button @click="$emit(\'click\')"><slot/></button>' } } },
|
||||
})
|
||||
|
||||
await wrapper.findAll('button').at(-1)!.trigger('click')
|
||||
expect(print).toHaveBeenCalled()
|
||||
expect(document.body.classList.contains('printing-dispatch-bol')).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,128 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount } from 'vue'
|
||||
import { Printer } from '@lucide/vue'
|
||||
import type { DispatchBolLineDto, DispatchItemDto, DispatchSummaryDto } from '../api/dispatches'
|
||||
|
||||
const props = defineProps<{ modelValue: boolean; detail: DispatchSummaryDto | null }>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
|
||||
|
||||
interface PrintLine {
|
||||
containerNo?: string | null
|
||||
palletNo?: string | null
|
||||
fbaWarehouseCode?: string | null
|
||||
destinationWarehouseName?: string | null
|
||||
warehouseNo?: string | null
|
||||
fbaShipmentId?: string | null
|
||||
poReferenceNo?: string | null
|
||||
packageCount: number
|
||||
}
|
||||
|
||||
function normalizeLine(line: DispatchBolLineDto): PrintLine {
|
||||
return { ...line, packageCount: Number(line.packageCount || 0) }
|
||||
}
|
||||
|
||||
function fallbackLine(line: DispatchItemDto): PrintLine {
|
||||
return {
|
||||
palletNo: line.palletNo,
|
||||
fbaWarehouseCode: line.fbaWarehouseCode || line.destinationWarehouseCode,
|
||||
destinationWarehouseName: line.destinationWarehouseName,
|
||||
warehouseNo: line.inboundCode,
|
||||
packageCount: Number(line.packageCount || 0),
|
||||
}
|
||||
}
|
||||
|
||||
function value(input: unknown) {
|
||||
const normalized = String(input ?? '').trim()
|
||||
return normalized || '—'
|
||||
}
|
||||
|
||||
function uniqueValues(lines: PrintLine[], key: 'fbaShipmentId' | 'poReferenceNo') {
|
||||
const values = lines.flatMap((line) => String(line[key] || '').split(','))
|
||||
.map((item) => item.trim()).filter(Boolean)
|
||||
return [...new Set(values)].join(', ') || '—'
|
||||
}
|
||||
|
||||
const groups = computed(() => {
|
||||
const source = props.detail?.bolLines?.length
|
||||
? props.detail.bolLines.map(normalizeLine)
|
||||
: (props.detail?.items || []).map(fallbackLine)
|
||||
const grouped = new Map<string, PrintLine[]>()
|
||||
source.forEach((line) => {
|
||||
const key = value(line.fbaWarehouseCode)
|
||||
grouped.set(key, [...(grouped.get(key) || []), line])
|
||||
})
|
||||
if (!grouped.size) grouped.set('—', [])
|
||||
return [...grouped.entries()].map(([fbaWarehouseCode, lines]) => ({
|
||||
fbaWarehouseCode,
|
||||
destinationWarehouseName: value(lines.find((line) => line.destinationWarehouseName)?.destinationWarehouseName),
|
||||
lines,
|
||||
totalPackages: lines.reduce((sum, line) => sum + line.packageCount, 0),
|
||||
shipmentIds: uniqueValues(lines, 'fbaShipmentId'),
|
||||
poReferenceNos: uniqueValues(lines, 'poReferenceNo'),
|
||||
blankRows: Array.from({ length: Math.max(0, 12 - lines.length) }, (_, index) => index),
|
||||
}))
|
||||
})
|
||||
|
||||
function cleanupPrintMode() { document.body.classList.remove('printing-dispatch-bol') }
|
||||
function printBol() {
|
||||
document.body.classList.add('printing-dispatch-bol')
|
||||
window.addEventListener('afterprint', cleanupPrintMode, { once: true })
|
||||
window.requestAnimationFrame(() => window.print())
|
||||
window.setTimeout(cleanupPrintMode, 3000)
|
||||
}
|
||||
|
||||
onBeforeUnmount(cleanupPrintMode)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog :model-value="modelValue" title="BOL 打印预览" width="min(1120px, calc(100vw - 48px))" append-to-body class="dispatch-bol-dialog" destroy-on-close @update:model-value="emit('update:modelValue', $event)">
|
||||
<div class="preview-note">按目的 FBA 仓自动分页。点击打印后可选择打印机,也可另存为 PDF。</div>
|
||||
<div class="dispatch-bol-pages">
|
||||
<article v-for="group in groups" :key="group.fbaWarehouseCode" class="dispatch-bol-sheet">
|
||||
<header class="bol-title">BILL OF LADING</header>
|
||||
<section class="bol-info-grid">
|
||||
<div class="bol-info-block"><h2>SHIP FROM</h2><div class="bol-info-content"><strong>{{ value(detail?.overseasWarehouseName) }}</strong><span>Address: —</span></div></div>
|
||||
<div class="bol-info-block"><h2>CARRIER</h2><div class="bol-info-content compact"><span>Carrier Name: <strong>{{ value(detail?.carrierName) }}</strong></span><span>ISA #: {{ value(detail?.fbaAppointmentNo) }}</span><span>Appointment time: {{ value(detail?.estimatedArrivalAt) }}</span><span>Trailer #: {{ value(detail?.vehiclePlate) }}</span><span>Seal #: —</span></div></div>
|
||||
<div class="bol-info-block"><h2>SHIP TO</h2><div class="bol-info-content compact"><span>Amazon FBA Warehouse</span><span>FC Code: <strong>{{ group.fbaWarehouseCode }}</strong></span><span>Name: {{ group.destinationWarehouseName }}</span><span>Address: —</span></div></div>
|
||||
<div class="bol-info-block"><h2>REFERENCE</h2><div class="bol-info-content compact"><span>BOL / Dispatch #: <strong>{{ value(detail?.dispatchNo) }}</strong></span><span>FBA Shipment ID: {{ group.shipmentIds }}</span><span>PO / Reference #: {{ group.poReferenceNos }}</span></div></div>
|
||||
</section>
|
||||
<table class="bol-table">
|
||||
<thead><tr><th>Container #</th><th>Pallet #</th><th>FBA Warehouse #</th><th>Warehouse No #</th><th>FBA Shipment ID</th><th>PO / Reference #</th><th>CTNS</th></tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="(line, index) in group.lines" :key="`${line.palletNo}-${line.warehouseNo}-${index}`"><td>{{ value(line.containerNo) }}</td><td>{{ value(line.palletNo) }}</td><td>{{ group.fbaWarehouseCode }}</td><td>{{ value(line.warehouseNo) }}</td><td>{{ value(line.fbaShipmentId) }}</td><td>{{ value(line.poReferenceNo) }}</td><td class="number-cell">{{ line.packageCount }}</td></tr>
|
||||
<tr v-for="blank in group.blankRows" :key="`blank-${blank}`" aria-hidden="true"><td> </td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<footer class="bol-footer"><div class="bol-total"><span>总箱数 / Total CTNS:</span><strong>{{ group.totalPackages }}</strong></div><div class="bol-signatures"><span>Driver signature</span><span>Warehouse signature</span><span>Date</span></div></footer>
|
||||
</article>
|
||||
</div>
|
||||
<template #footer><el-button @click="emit('update:modelValue', false)">关闭</el-button><el-button type="primary" @click="printBol"><Printer :size="16" />打印 / 另存 PDF</el-button></template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.preview-note{margin-bottom:16px;color:var(--cc-text-secondary);font-size:14px}.dispatch-bol-pages{display:grid;gap:20px;padding:16px;background:#e9edf2}.dispatch-bol-sheet{box-sizing:border-box;width:210mm;min-height:297mm;margin:0 auto;padding:9mm;background:#fff;color:#111;font-family:Arial,"Microsoft YaHei",sans-serif;box-shadow:0 2px 12px rgb(15 23 42 / 16%)}.bol-title{padding:5px 10px;background:#050505;color:#fff;text-align:center;font-family:"Times New Roman",serif;font-size:24px;font-weight:700;letter-spacing:1px}.bol-info-grid{display:grid;grid-template-columns:1fr 1fr;border-left:1px solid #111}.bol-info-block{min-height:108px;border-right:1px solid #111;border-bottom:1px solid #111}.bol-info-block h2{margin:0;padding:3px 6px;background:#050505;color:#fff;font-family:"Times New Roman",serif;font-size:14px;line-height:1.2}.bol-info-content{display:flex;flex-direction:column;gap:4px;padding:10px 8px;font-size:15px;line-height:1.25}.bol-info-content>strong{font-size:18px}.bol-info-content.compact{gap:2px;padding-top:6px}.bol-table{width:100%;border-collapse:collapse;table-layout:fixed;font-size:10px}.bol-table th,.bol-table td{height:30px;padding:3px 4px;border:1px solid #111;text-align:center;vertical-align:middle;overflow-wrap:anywhere}.bol-table th{font-weight:700}.bol-table th:nth-child(1){width:11%}.bol-table th:nth-child(2){width:14%}.bol-table th:nth-child(3){width:13%}.bol-table th:nth-child(4){width:17%}.bol-table th:nth-child(5){width:20%}.bol-table th:nth-child(6){width:17%}.bol-table th:nth-child(7){width:8%}.number-cell{text-align:right!important;font-size:12px}.bol-footer{display:flex;flex-direction:column;justify-content:space-between;min-height:42mm;padding:10mm 4mm 0}.bol-total{display:flex;justify-content:space-between;border-bottom:1px solid #111;font-family:"Times New Roman",serif;font-size:14px}.bol-total strong{font-size:20px}.bol-signatures{display:grid;grid-template-columns:2fr 2fr 1fr;gap:16px}.bol-signatures span{padding-top:18px;border-top:1px solid #111;font-size:11px;text-align:center}@media(max-width:900px){.dispatch-bol-pages{overflow:auto}.dispatch-bol-sheet{margin:0}}
|
||||
</style>
|
||||
<style>
|
||||
@media print{
|
||||
@page{size:A4 portrait;margin:0}
|
||||
html,body.printing-dispatch-bol{width:auto!important;height:auto!important;min-height:0!important;overflow:visible!important;background:#fff!important}
|
||||
body.printing-dispatch-bol{margin:0!important}
|
||||
body.printing-dispatch-bol>#app{display:none!important}
|
||||
body.printing-dispatch-bol *{visibility:hidden!important}
|
||||
body.printing-dispatch-bol .el-overlay,
|
||||
body.printing-dispatch-bol .el-overlay-dialog,
|
||||
body.printing-dispatch-bol .dispatch-bol-dialog,
|
||||
body.printing-dispatch-bol .dispatch-bol-dialog .el-dialog__body{position:static!important;inset:auto!important;width:auto!important;height:auto!important;max-width:none!important;max-height:none!important;margin:0!important;padding:0!important;overflow:visible!important;transform:none!important;background:transparent!important;box-shadow:none!important}
|
||||
body.printing-dispatch-bol .dispatch-bol-dialog .el-dialog__header,
|
||||
body.printing-dispatch-bol .dispatch-bol-dialog .el-dialog__footer,
|
||||
body.printing-dispatch-bol .preview-note{display:none!important}
|
||||
body.printing-dispatch-bol .dispatch-bol-pages,
|
||||
body.printing-dispatch-bol .dispatch-bol-pages *{visibility:visible!important}
|
||||
body.printing-dispatch-bol .dispatch-bol-pages{position:static!important;inset:auto!important;display:block!important;width:auto!important;height:auto!important;margin:0!important;padding:0!important;overflow:visible!important;background:#fff!important}
|
||||
body.printing-dispatch-bol .dispatch-bol-sheet{width:210mm;min-height:297mm;margin:0;padding:9mm;overflow:visible!important;box-shadow:none;break-after:page;page-break-after:always}
|
||||
body.printing-dispatch-bol .dispatch-bol-sheet:last-child{break-after:auto;page-break-after:auto}
|
||||
body.printing-dispatch-bol .bol-table thead{display:table-header-group}
|
||||
body.printing-dispatch-bol .bol-table tr{break-inside:avoid;page-break-inside:avoid}
|
||||
}
|
||||
</style>
|
||||
@@ -4,12 +4,12 @@ import { resolve } from 'node:path'
|
||||
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
||||
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import QueryToolbar, { type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import { cancelDispatch, getDispatch, listDispatches, transferDispatchToDelivery, updateDispatch } from '../api/dispatches'
|
||||
import { cancelDispatch, getDispatchBolPdf, listDispatches, transferDispatchToDelivery, updateDispatch } from '../api/dispatches'
|
||||
import DispatchesView from './DispatchesView.vue'
|
||||
|
||||
const canPermission = vi.hoisted(() => vi.fn(() => true))
|
||||
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: canPermission }) }))
|
||||
vi.mock('../api/dispatches', () => ({ listDispatches: vi.fn(), getDispatch: vi.fn(), updateDispatch: vi.fn(), cancelDispatch: vi.fn(), transferDispatchToDelivery: vi.fn() }))
|
||||
vi.mock('../api/dispatches', () => ({ listDispatches: vi.fn(), getDispatchBolPdf: vi.fn(), updateDispatch: vi.fn(), cancelDispatch: vi.fn(), transferDispatchToDelivery: vi.fn() }))
|
||||
|
||||
class ResizeObserverStub implements ResizeObserver {
|
||||
observe() {}
|
||||
@@ -47,7 +47,7 @@ describe('DispatchesView', () => {
|
||||
page: 1, size: 20, total: 1, pages: 1,
|
||||
})
|
||||
vi.mocked(updateDispatch).mockReset()
|
||||
vi.mocked(getDispatch).mockReset().mockResolvedValue({ id: '190000000000000099', dispatchNo: 'PC-1', bolLines: [] })
|
||||
vi.mocked(getDispatchBolPdf).mockReset().mockResolvedValue(new Blob(['%PDF-test'], { type: 'application/pdf' }))
|
||||
vi.mocked(cancelDispatch).mockReset()
|
||||
vi.mocked(transferDispatchToDelivery).mockReset()
|
||||
canPermission.mockReset().mockReturnValue(true)
|
||||
@@ -85,17 +85,20 @@ describe('DispatchesView', () => {
|
||||
expect(wrapper.html()).toContain('190000000000000099')
|
||||
})
|
||||
|
||||
it('loads one detail request only when BOL preview is opened', async () => {
|
||||
it('opens the server-generated BOL PDF with one request', async () => {
|
||||
const pdfWindow = { location: { href: '' }, close: vi.fn() }
|
||||
vi.spyOn(window, 'open').mockReturnValue(pdfWindow as unknown as Window)
|
||||
vi.stubGlobal('URL', { createObjectURL: vi.fn(() => 'blob:dispatch-bol'), revokeObjectURL: vi.fn() })
|
||||
const wrapper = mount(DispatchesView, { global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
expect(getDispatch).not.toHaveBeenCalled()
|
||||
expect(getDispatchBolPdf).not.toHaveBeenCalled()
|
||||
|
||||
await wrapper.findAll('.el-table__body-wrapper button').find((button) => button.text().trim() === '打印BOL')!.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(getDispatch).toHaveBeenCalledOnce()
|
||||
expect(getDispatch).toHaveBeenCalledWith('PC-1')
|
||||
expect(document.body.textContent).toContain('BOL 打印预览')
|
||||
expect(getDispatchBolPdf).toHaveBeenCalledOnce()
|
||||
expect(getDispatchBolPdf).toHaveBeenCalledWith('PC-1')
|
||||
expect(pdfWindow.location.href).toBe('blob:dispatch-bol')
|
||||
})
|
||||
|
||||
it('documents the backend feature flag required by a missing endpoint', () => {
|
||||
|
||||
@@ -13,9 +13,8 @@ import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/compone
|
||||
import StatusBadge from '@/shared/components/StatusBadge.vue'
|
||||
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { cancelDispatch, getDispatch, listDispatches, transferDispatchToDelivery, updateDispatch, type DispatchSummaryDto } from '../api/dispatches'
|
||||
import { cancelDispatch, getDispatchBolPdf, listDispatches, transferDispatchToDelivery, updateDispatch, type DispatchSummaryDto } from '../api/dispatches'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import DispatchBolPrintDialog from '../components/DispatchBolPrintDialog.vue'
|
||||
import type { DispatchListItem } from '../types'
|
||||
import { formatOverseasDateTime } from '../lastMilePresentation'
|
||||
import { overseasWarehousePermissions as permission } from '../permissions'
|
||||
@@ -32,7 +31,7 @@ const filters = ref<QueryModel>({ dispatchNo: '', vehiclePlate: '', status: '',
|
||||
const transferOpen = ref(false); const transferRow = ref<DispatchListItem | null>(null); const transferSaving = ref(false)
|
||||
const mutationError = ref<ApiErrorDetails | null>(null)
|
||||
const editOpen = ref(false); const editRow = ref<DispatchListItem | null>(null); const editSaving = ref(false)
|
||||
const bolOpen = ref(false); const bolDetail = ref<DispatchSummaryDto | null>(null); const bolLoadingNo = ref('')
|
||||
const bolLoadingNo = ref('')
|
||||
const dispatchSnapshots = new Map<string, DispatchSummaryDto>()
|
||||
const editForm = reactive({ vehiclePlate: '', driverName: '', driverPhone: '', carrierName: '', route: '', departAt: '', fbaAppointmentNo: '', fbaAppointmentStatus: '', vehicleAppointmentStatus: '', estimatedArrivalAt: '' })
|
||||
const transferForm = reactive({ plannedDeliveryAt: '', deliveryMethod: '', waybillNo: '', carrierName: '', pickupMethod: '', loadingType: '', vehicleType: '', vehicleSize: '', driverName: '', driverPhone: '', vehiclePlate: '', appointmentAccount: '', sellerName: '', deliveryFee: undefined as number | undefined, loadingFee: undefined as number | undefined, appointmentFee: undefined as number | undefined, cartonMarksJson: '', remark: '' })
|
||||
@@ -189,13 +188,18 @@ function reset(model: QueryModel) { page.value = 1; void load(model) }
|
||||
function openDetail(row: DispatchListItem) { void router.push({ name: 'overseas-dispatch-detail', params: { dispatchNo: row.dispatchNo } }) }
|
||||
async function openBol(row: DispatchListItem) {
|
||||
if (bolLoadingNo.value) return
|
||||
const pdfWindow = window.open('', '_blank')
|
||||
if (!pdfWindow) { ElMessage.warning('浏览器拦截了 BOL PDF 窗口,请允许本站打开新窗口'); return }
|
||||
bolLoadingNo.value = row.dispatchNo
|
||||
mutationError.value = null
|
||||
try {
|
||||
bolDetail.value = await getDispatch(row.dispatchNo)
|
||||
bolOpen.value = true
|
||||
const pdf = await getDispatchBolPdf(row.dispatchNo)
|
||||
const url = URL.createObjectURL(pdf)
|
||||
pdfWindow.location.href = url
|
||||
window.setTimeout(() => URL.revokeObjectURL(url), 60_000)
|
||||
} catch (cause) {
|
||||
mutationError.value = getApiErrorDetails(cause, 'BOL 打印数据加载失败,请重试')
|
||||
pdfWindow.close()
|
||||
mutationError.value = getApiErrorDetails(cause, 'BOL PDF 生成失败,请重试')
|
||||
} finally {
|
||||
bolLoadingNo.value = ''
|
||||
}
|
||||
@@ -238,7 +242,6 @@ onMounted(load)
|
||||
<template #status="{ row }"><StatusBadge :status="String((row as DispatchListItem).status || '—')" /></template>
|
||||
<template #pagination-left>共 {{ total }} 条排车记录</template>
|
||||
</DataTable></section>
|
||||
<DispatchBolPrintDialog v-model="bolOpen" :detail="bolDetail" />
|
||||
</ListPageLayout></AdminPage><AppDialog v-model="transferOpen" title="转派送" width="min(1120px, calc(100vw - 28px))" confirm-text="确认提交" :confirm-loading="transferSaving" @confirm="submitTransfer"><el-form label-position="top"><h3>派送信息</h3><div class="transfer-grid"><el-form-item label="排车单号"><el-input :model-value="transferRow?.dispatchNo" readonly /></el-form-item><el-form-item label="预计派送时间" required><el-date-picker v-model="transferForm.plannedDeliveryAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择预计派送时间" /></el-form-item><el-form-item label="派送方式" required><el-select v-model="transferForm.deliveryMethod" placeholder="请选择"><el-option v-for="option in deliveryMethodOptions" :key="option.value" v-bind="option" /></el-select></el-form-item><el-form-item label="快递/车队"><el-input v-model="transferForm.carrierName" placeholder="请输入快递或车队名称" /></el-form-item><el-form-item label="运单号"><el-input v-model="transferForm.waybillNo" placeholder="请输入运单号" /></el-form-item><el-form-item label="提货方式" required><el-select v-model="transferForm.pickupMethod" placeholder="请选择"><el-option v-for="option in pickupMethodOptions" :key="option.value" v-bind="option" /></el-select></el-form-item><el-form-item label="装车类型" required><el-select v-model="transferForm.loadingType" placeholder="请选择"><el-option v-for="option in loadingTypeOptions" :key="option.value" v-bind="option" /></el-select></el-form-item><el-form-item label="司机/车队名称"><el-input v-model="transferForm.driverName" placeholder="请输入司机或车队名称" /></el-form-item><el-form-item label="联系电话"><el-input v-model="transferForm.driverPhone" placeholder="请输入联系电话" /></el-form-item><el-form-item label="车牌号"><el-input v-model="transferForm.vehiclePlate" placeholder="请输入车牌号" /></el-form-item><el-form-item label="车辆类型"><el-select v-model="transferForm.vehicleType" clearable placeholder="请选择"><el-option v-for="option in vehicleTypeOptions" :key="option.value" v-bind="option" /></el-select></el-form-item><el-form-item label="车辆大小"><el-select v-model="transferForm.vehicleSize" clearable placeholder="请选择"><el-option v-for="option in vehicleSizeOptions" :key="option.value" v-bind="option" /></el-select></el-form-item><el-form-item label="预约账号"><el-select v-model="transferForm.appointmentAccount" clearable placeholder="请选择"><el-option v-for="option in appointmentAccountOptions" :key="option.value" v-bind="option" /></el-select></el-form-item><el-form-item v-if="isBuyAppointment" label="卖家姓名" required><el-input v-model="transferForm.sellerName" placeholder="请输入卖家姓名" /></el-form-item></div><el-form-item label="托盘箱唛确认"><el-input v-model="transferForm.cartonMarksJson" type="textarea" :rows="3" placeholder="按实际托盘填写 JSON,例如 {"PALLET-001":["箱唛1","箱唛2"]}" /><div class="form-hint">入库未逐箱绑定时,在出库前确认箱唛属于哪个实际托盘。</div></el-form-item><h3>费用信息(USD)</h3><div class="transfer-grid"><el-form-item label="派送费用" required><el-input-number v-model="transferForm.deliveryFee" :min="0" :precision="2" controls-position="right" placeholder="请输入派送费用" /></el-form-item><el-form-item v-if="needsLoadingFee" label="装车费" required><el-input-number v-model="transferForm.loadingFee" :min="0" :precision="2" controls-position="right" placeholder="请输入装车费" /></el-form-item><el-form-item v-if="isBuyAppointment" label="买约费用" required><el-input-number v-model="transferForm.appointmentFee" :min="0" :precision="2" controls-position="right" placeholder="请输入买约费用" /></el-form-item></div><el-form-item label="备注"><el-input v-model="transferForm.remark" type="textarea" :rows="3" placeholder="请输入派送备注(选填)" /></el-form-item><el-alert title="费用不会自动带出,请手工填写后提交;提交后将生成待出库单、待派送单,并保存费用明细" type="info" :closable="false" /></el-form></AppDialog>
|
||||
<AppDialog v-model="editOpen" title="编辑排车" width="min(900px, calc(100vw - 48px))" confirm-text="保存" :confirm-loading="editSaving" @confirm="submitEdit"><el-alert :title="`当前状态:${editRow?.status || '—'};普通编辑不会修改排车状态。`" type="info" :closable="false" /><el-form label-position="top"><div class="edit-grid"><el-form-item label="车牌号"><el-input v-model="editForm.vehiclePlate" placeholder="请输入车牌号" /></el-form-item><el-form-item label="司机"><el-input v-model="editForm.driverName" placeholder="请输入司机姓名" /></el-form-item><el-form-item label="联系电话"><el-input v-model="editForm.driverPhone" placeholder="请输入联系电话" /></el-form-item><el-form-item label="拖车公司名称"><el-input v-model="editForm.carrierName" placeholder="请输入拖车公司名称" /></el-form-item><el-form-item label="路线"><el-input v-model="editForm.route" placeholder="请输入运输路线" /></el-form-item><el-form-item label="发车时间"><el-date-picker v-model="editForm.departAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" /></el-form-item><el-form-item label="预计到达时间"><el-date-picker v-model="editForm.estimatedArrivalAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" /></el-form-item><el-form-item label="FBA预约号"><el-input v-model="editForm.fbaAppointmentNo" placeholder="请输入 FBA 预约号" /></el-form-item><el-form-item label="FBA预约状态"><el-select v-model="editForm.fbaAppointmentStatus" clearable><el-option label="待预约" value="PENDING" /><el-option label="已预约" value="BOOKED" /><el-option label="已确认" value="CONFIRMED" /><el-option label="已取消" value="CANCELLED" /></el-select></el-form-item><el-form-item label="车辆预约状态"><el-select v-model="editForm.vehicleAppointmentStatus" clearable><el-option label="待预约" value="PENDING" /><el-option label="已预约" value="BOOKED" /><el-option label="已确认" value="CONFIRMED" /><el-option label="已取消" value="CANCELLED" /></el-select></el-form-item></div></el-form></AppDialog>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user