feat: add dispatch BOL printing
This commit is contained in:
@@ -85,6 +85,18 @@ export interface DispatchSummaryDto {
|
||||
partialCompletionReason?: string | null
|
||||
createdAt?: string | null
|
||||
items?: DispatchItemDto[]
|
||||
bolLines?: DispatchBolLineDto[]
|
||||
}
|
||||
|
||||
export interface DispatchBolLineDto {
|
||||
containerNo?: string | null
|
||||
palletNo?: string | null
|
||||
fbaWarehouseCode?: string | null
|
||||
destinationWarehouseName?: string | null
|
||||
warehouseNo?: string | null
|
||||
fbaShipmentId?: string | null
|
||||
poReferenceNo?: string | null
|
||||
packageCount: number
|
||||
}
|
||||
|
||||
export interface UpdateDispatchPayload {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
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)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,108 @@
|
||||
<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}body.printing-dispatch-bol *{visibility:hidden!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:absolute;inset:0;display:block;padding:0;background:#fff}body.printing-dispatch-bol .dispatch-bol-sheet{width:210mm;min-height:297mm;margin:0;padding:9mm;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, listDispatches, transferDispatchToDelivery, updateDispatch } from '../api/dispatches'
|
||||
import { cancelDispatch, getDispatch, 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(), updateDispatch: vi.fn(), cancelDispatch: vi.fn(), transferDispatchToDelivery: vi.fn() }))
|
||||
vi.mock('../api/dispatches', () => ({ listDispatches: vi.fn(), getDispatch: vi.fn(), updateDispatch: vi.fn(), cancelDispatch: vi.fn(), transferDispatchToDelivery: vi.fn() }))
|
||||
|
||||
class ResizeObserverStub implements ResizeObserver {
|
||||
observe() {}
|
||||
@@ -47,6 +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(cancelDispatch).mockReset()
|
||||
vi.mocked(transferDispatchToDelivery).mockReset()
|
||||
canPermission.mockReset().mockReturnValue(true)
|
||||
@@ -84,6 +85,19 @@ describe('DispatchesView', () => {
|
||||
expect(wrapper.html()).toContain('190000000000000099')
|
||||
})
|
||||
|
||||
it('loads one detail request only when BOL preview is opened', async () => {
|
||||
const wrapper = mount(DispatchesView, { global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
expect(getDispatch).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 打印预览')
|
||||
})
|
||||
|
||||
it('documents the backend feature flag required by a missing endpoint', () => {
|
||||
const source = readFileSync(resolve(process.cwd(), 'src/modules/overseas-warehouse/pages/DispatchesView.vue'), 'utf8')
|
||||
expect(source).toContain('请确认海外尾程排车接口已发布')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { Eye, MoreHorizontal, Pencil, Truck, XCircle } from '@lucide/vue'
|
||||
import { Eye, MoreHorizontal, Pencil, Printer, Truck, XCircle } from '@lucide/vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
@@ -13,8 +13,9 @@ 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, listDispatches, transferDispatchToDelivery, updateDispatch, type DispatchSummaryDto } from '../api/dispatches'
|
||||
import { cancelDispatch, getDispatch, 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'
|
||||
@@ -31,6 +32,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 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: '' })
|
||||
@@ -74,7 +76,7 @@ const fields = computed<QueryField[]>(() => [
|
||||
])
|
||||
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 270, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'actions', label: '操作', width: 380, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'dispatchNo', prop: 'dispatchNo', label: '排车单号', minWidth: 170, fixed: 'left' },
|
||||
{ key: 'vehiclePlate', prop: 'vehiclePlate', label: '车牌号', minWidth: 130 },
|
||||
{ key: 'driverName', prop: 'driverName', label: '司机', minWidth: 120 },
|
||||
@@ -185,6 +187,19 @@ async function load(model: QueryModel = filters.value) {
|
||||
function search(model: QueryModel) { page.value = 1; void load(model) }
|
||||
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
|
||||
bolLoadingNo.value = row.dispatchNo
|
||||
mutationError.value = null
|
||||
try {
|
||||
bolDetail.value = await getDispatch(row.dispatchNo)
|
||||
bolOpen.value = true
|
||||
} catch (cause) {
|
||||
mutationError.value = getApiErrorDetails(cause, 'BOL 打印数据加载失败,请重试')
|
||||
} finally {
|
||||
bolLoadingNo.value = ''
|
||||
}
|
||||
}
|
||||
function isPendingDeparture(row: DispatchListItem) { return row.statusCode === 'PENDING_DEPARTURE' }
|
||||
function canEdit(row: DispatchListItem) { return access.can(permission.dispatchEdit) && isPendingDeparture(row) }
|
||||
function canCancel(row: DispatchListItem) { return access.can(permission.dispatchCancel) && row.statusCode === 'PENDING_DEPARTURE' }
|
||||
@@ -219,10 +234,11 @@ onMounted(load)
|
||||
<template #toolbar><QueryToolbar v-model="filters" class="surface-panel" :fields="fields" :loading="loading" ariaLabel="排车列表筛选" preference-key="overseas-dispatches-query" :search-on-reset="false" @search="search" @reset="reset" /></template>
|
||||
<template v-if="loadError" #state><LoadErrorState title="排车列表加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState></template>
|
||||
<section v-if="!loadError" class="surface-panel table-panel"><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" preference-key="overseas-dispatches" row-key="id" empty-text="暂无排车记录" @update:page="load()" @update:page-size="page = 1; load()">
|
||||
<template #actions="{ row }"><el-button size="small" plain type="primary" @click="openDetail(row as DispatchListItem)"><Eye :size="15" />查看</el-button><el-button v-if="canTransfer(row as DispatchListItem)" size="small" type="success" plain @click="openTransfer(row as DispatchListItem)"><Truck :size="15" />转派送</el-button><el-dropdown v-if="canEdit(row as DispatchListItem) || canCancel(row as DispatchListItem)" trigger="click" @command="handleMore(String($event), row as DispatchListItem)"><el-button size="small" plain><MoreHorizontal :size="15" />更多</el-button><template #dropdown><el-dropdown-menu><el-dropdown-item v-if="canEdit(row as DispatchListItem)" command="edit"><Pencil :size="15" />编辑</el-dropdown-item><el-dropdown-item v-if="canCancel(row as DispatchListItem)" command="cancel" divided><XCircle :size="15" />取消</el-dropdown-item></el-dropdown-menu></template></el-dropdown></template>
|
||||
<template #actions="{ row }"><el-button size="small" plain type="primary" @click="openDetail(row as DispatchListItem)"><Eye :size="15" />查看</el-button><el-button size="small" plain :loading="bolLoadingNo === (row as DispatchListItem).dispatchNo" @click="openBol(row as DispatchListItem)"><Printer :size="15" />打印BOL</el-button><el-button v-if="canTransfer(row as DispatchListItem)" size="small" type="success" plain @click="openTransfer(row as DispatchListItem)"><Truck :size="15" />转派送</el-button><el-dropdown v-if="canEdit(row as DispatchListItem) || canCancel(row as DispatchListItem)" trigger="click" @command="handleMore(String($event), row as DispatchListItem)"><el-button size="small" plain><MoreHorizontal :size="15" />更多</el-button><template #dropdown><el-dropdown-menu><el-dropdown-item v-if="canEdit(row as DispatchListItem)" command="edit"><Pencil :size="15" />编辑</el-dropdown-item><el-dropdown-item v-if="canCancel(row as DispatchListItem)" command="cancel" divided><XCircle :size="15" />取消</el-dropdown-item></el-dropdown-menu></template></el-dropdown></template>
|
||||
<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