@@ -1,6 +1,6 @@
< script setup lang = "ts" >
import { computed , onMounted , reactive , ref } from 'vue'
import { Eye , MoreHorizontal , Pencil , Printer , Truck , XCircle } from '@lucide/vue'
import { Eye , MoreHorizontal , Pencil , Printer , Trash2 , 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,7 +13,7 @@ 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 , getDispatchBolPdf , listDispatches , transferDispatchToDelivery , updateDispatch , type DispatchSummaryDto } from '../api/dispatches'
import { cancelDispatch , getDispatchBolPdf , listDispatches , removeDispatchItem , transferDispatchToDelivery , updateDispatch , type DispatchItemDto , type DispatchSummaryDto } from '../api/dispatches'
import AppDialog from '@/shared/components/AppDialog.vue'
import type { DispatchListItem } from '../types'
import { formatOverseasDateTime } from '../lastMilePresentation'
@@ -27,32 +27,86 @@ const loadError = ref<ApiErrorDetails | null>(null)
const page = ref ( 1 )
const pageSize = ref ( 10 )
const total = ref ( 0 )
const filters = ref < QueryModel > ( { dispatchNo : '' , vehiclePlate : '' , status : '' , fbaAppointmentNo : '' } )
const transferOpen = ref ( false ) ; const transferRow = ref < DispatchListItem | null > ( null ) ; const transferSaving = ref ( false )
const filters = ref < QueryModel > ( {
dispatchNo : '' ,
vehiclePlate : '' ,
status : '' ,
fbaAppointmentNo : '' ,
} )
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 editOpen = ref ( false )
const editRow = ref < DispatchListItem | null > ( null )
const editSaving = ref ( false )
const editItems = ref < DispatchItemDto [ ] > ( [ ] )
const removingItemId = 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 : '' } )
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 : '' ,
} )
const deliveryMethodOptions = [
{ label : 'FBA自提' , value : 'FBA_PICKUP' } , { label : '客户自提' , value : 'CUSTOMER_PICKUP' } ,
{ label : '卡派打托 ' , value : 'TRUCK_PALLET' } , { label : '卡派地板' , value : 'TRUCK_FLOOR ' } ,
{ label : '快递 ' , value : 'EXPRESS' } , { label : '外配' , value : 'OUTSOURCED ' } ,
{ label : '专车直送 ' , value : 'DEDICATED_TRUCK' } , { label : '零担派送' , value : 'LTL ' } ,
{ label : 'FBA自提' , value : 'FBA_PICKUP' } ,
{ label : '客户自提 ' , value : 'CUSTOMER_PICKUP ' } ,
{ label : '卡派打托 ' , value : 'TRUCK_PALLET ' } ,
{ label : '卡派地板 ' , value : 'TRUCK_FLOOR ' } ,
{ label : '快递' , value : 'EXPRESS' } ,
{ label : '外配' , value : 'OUTSOURCED' } ,
{ label : '专车直送' , value : 'DEDICATED_TRUCK' } ,
{ label : '零担派送' , value : 'LTL' } ,
]
const pickupMethodOptions = [
{ label : '一提一卸' , value : 'ONE_PICKUP_ONE_DROP' } , { label : '一提多卸' , value : 'ONE_PICKUP_MULTI_DROP' } ,
{ label : '多提 一卸' , value : 'MULTI_PICKUP_ONE_DROP' } , { label : '多提多卸' , value : 'MULTI _PICKUP_MULTI_DROP' } ,
{ label : '一提一卸' , value : 'ONE_PICKUP_ONE_DROP' } ,
{ label : '一提多 卸' , value : 'ONE _PICKUP_MULTI_DROP' } ,
{ label : '多提一卸' , value : 'MULTI_PICKUP_ONE_DROP' } ,
{ label : '多提多卸' , value : 'MULTI_PICKUP_MULTI_DROP' } ,
{ label : '仓库自提' , value : 'WAREHOUSE_PICKUP' } ,
]
const loadingTypeOptions = [ { label : '托盘' , value : 'PALLET' } , { label : '地板' , value : 'FLOOR' } , { label : '混装' , value : 'MIXED' } ]
const loadingTypeOptions = [
{ label : '托盘' , value : 'PALLET' } ,
{ label : '地板' , value : 'FLOOR' } ,
{ label : '混装' , value : 'MIXED' } ,
]
const vehicleTypeOptions = [ '厢式货车' , '平板车' , '冷藏车' , '集装箱卡车' , '尾板车' ] . map ( ( label ) => ( { label , value : label } ) )
const vehicleSizeOptions = [ '53尺大车(30托)' , '26尺中车(14托)' , '16尺小车(8托)' , 'Cargo Van( 4托)' ] . map ( ( label ) => ( { label , value : label } ) )
const appointmentAccountOptions = [
{ label : '4号账号(美西)' , value : '4' } , { label : '8号账号(美东)' , value : '8' } ,
{ label : '10 号账号(美中 ) ' , value : '10' } , { label : '12号账号(加拿大)' , value : '12' } , { label : '买约' , value : 'BUY_APPOINTMENT ' } ,
{ label : '4号账号(美西)' , value : '4' } ,
{ label : '8 号账号(美东 ) ' , value : '8 ' } ,
{ label : '10号账号(美中)' , value : '10' } ,
{ label : '12号账号(加拿大)' , value : '12' } ,
{ label : '买约' , value : 'BUY_APPOINTMENT' } ,
]
const isBuyAppointment = computed ( ( ) => transferForm . appointmentAccount === 'BUY_APPOINTMENT' )
const needsLoadingFee = computed ( ( ) => [ 'FLOOR' , 'MIXED' ] . includes ( transferForm . loadingType ) )
@@ -68,10 +122,35 @@ const statusOptions = [
]
const fields = computed < QueryField [ ] > ( ( ) => [
{ key : 'dispatchNo' , type : 'input' , label : '排车单号' , placeholder : '请输入排车单号' , width : 220 } ,
{ key : 'vehiclePlate' , type : 'input' , label : '车牌号' , placeholder : '请输入车牌号' , width : 180 } ,
{ key : 'status' , type : 'select' , label : '车辆状态' , placeholder : '全部状态' , options : statusOptions , width : 180 } ,
{ key : 'fbaAppointmentNo' , type : 'input' , label : 'FBA预约号' , placeholder : '请输入FBA预约号' , width : 220 } ,
{
key : 'dispatchNo' ,
type : 'input' ,
label : '排车单号' ,
placeholder : '请输入排车单号' ,
width : 220 ,
} ,
{
key : 'vehiclePlate' ,
type : 'input' ,
label : '车牌号' ,
placeholder : '请输入车牌号' ,
width : 180 ,
} ,
{
key : 'status' ,
type : 'select' ,
label : '车辆状态' ,
placeholder : '全部状态' ,
options : statusOptions ,
width : 180 ,
} ,
{
key : 'fbaAppointmentNo' ,
type : 'input' ,
label : 'FBA预约号' ,
placeholder : '请输入FBA预约号' ,
width : 220 ,
} ,
] )
const columns : DataTableColumn [ ] = [
@@ -80,17 +159,60 @@ const columns: DataTableColumn[] = [
{ key : 'vehiclePlate' , prop : 'vehiclePlate' , label : '车牌号' , minWidth : 130 } ,
{ key : 'driverName' , prop : 'driverName' , label : '司机' , minWidth : 120 } ,
{ key : 'driverPhone' , prop : 'driverPhone' , label : '联系电话' , minWidth : 140 } ,
{ key : 'carrierName' , prop : 'carrierName' , label : '托车公司名称' , minWidth : 160 } ,
{
key : 'carrierName' ,
prop : 'carrierName' ,
label : '托车公司名称' ,
minWidth : 160 ,
} ,
{ key : 'orderNos' , prop : 'orderNos' , label : '关联订单' , minWidth : 220 } ,
{ key : 'route' , prop : 'route' , label : '路线' , minWidth : 180 } ,
{ key : 'overseasWarehouse' , prop : 'overseasWarehouse' , label : '海外仓' , minWidth : 160 } ,
{
key : 'overseasWarehouse' ,
prop : 'overseasWarehouse' ,
label : '海外仓' ,
minWidth : 160 ,
} ,
{ key : 'departAt' , prop : 'departAt' , label : '发车时间' , minWidth : 170 } ,
{ key : 'fbaAppointment' , prop : 'fbaAppointment' , label : 'FBA预约' , minWidth : 180 } ,
{ key : 'vehicleAppointment' , prop : 'vehicleAppointment' , label : '车辆预约' , minWidth : 130 } ,
{ key : 'estimatedArrivalAt' , prop : 'estimatedArrivalAt' , label : '预计到达时间' , minWidth : 170 } ,
{ key : 'palletCount' , prop : 'palletCount' , label : '托盘数' , width : 90 , align : 'right' } ,
{ key : 'packageCount' , prop : 'packageCount' , label : '箱数' , width : 80 , align : 'right' } ,
{ key : 'status' , prop : 'status' , label : '车辆状态' , width : 150 , slotName : 'status' } ,
{
key : 'fbaAppointment' ,
prop : 'fbaAppointment' ,
label : 'FBA预约' ,
minWidth : 180 ,
} ,
{
key : 'vehicleAppointment' ,
prop : 'vehicleAppointment' ,
label : '车辆预约' ,
minWidth : 130 ,
} ,
{
key : 'estimatedArrivalAt' ,
prop : 'estimatedArrivalAt' ,
label : '预计到达时间' ,
minWidth : 170 ,
} ,
{
key : 'palletCount' ,
prop : 'palletCount' ,
label : '托盘数' ,
width : 90 ,
align : 'right' ,
} ,
{
key : 'packageCount' ,
prop : 'packageCount' ,
label : '箱数' ,
width : 80 ,
align : 'right' ,
} ,
{
key : 'status' ,
prop : 'status' ,
label : '车辆状态' ,
width : 150 ,
slotName : 'status' ,
} ,
{ key : 'createdAt' , prop : 'createdAt' , label : '创建时间' , minWidth : 170 } ,
]
@@ -153,7 +275,10 @@ function toListItem(dto: DispatchSummaryDto): DispatchListItem {
function dispatchError ( error : unknown ) {
const details = getApiErrorDetails ( error , '排车列表加载失败,请重试' )
return details . code === 'HTTP_404'
? { ... details , message : '排车接口未启用或不存在,请确认海外尾程排车接口已发布' }
? {
... details ,
message : '排车接口未启用或不存在,请确认海外尾程排车接口已发布' ,
}
: details
}
@@ -183,9 +308,20 @@ 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 } } ) }
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
const pdfWindow = window . open ( '' , '_blank' )
@@ -204,29 +340,211 @@ async function openBol(row: DispatchListItem) {
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' }
function canTransfer ( row : DispatchListItem ) { return access . can ( permission . dispatchTransferDelivery ) && [ 'PENDING_DEPARTURE' , 'PICKING' , 'READY_TO_DEPART' ] . includes ( row . statusCode ) }
function openEdit ( row : DispatchListItem ) { if ( ! canEdit ( row ) ) return ; const source = dispatchSnapshots . get ( row . dispatchNo ) ; editRow . value = row ; Object . assign ( editForm , { vehiclePlate : source ? . vehiclePlate || '' , driverName : source ? . driverName || '' , driverPhone : source ? . driverPhone || '' , carrierName : source ? . carrierName || '' , route : source ? . route || '' , departAt : source ? . departAt || '' , fbaAppointmentNo : source ? . fbaAppointmentNo || '' , fbaAppointmentStatus : source ? . fbaAppointmentStatus || '' , vehicleAppointmentStatus : source ? . vehicleAppointmentStatus || '' , estimatedArrivalAt : source ? . estimatedArrivalAt || '' } ) ; mutationError . value = null ; editOpen . value = true }
function openTransfer ( row : DispatchListItem ) { if ( ! canTransfer ( row ) ) return ; transferRow . value = row ; Object . assign ( transferForm , { plannedDeliveryAt : '' , deliveryMethod : '' , waybillNo : '' , carrierName : row . carrierName === '—' ? '' : row . carrierName , pickupMethod : '' , loadingType : '' , vehicleType : '' , vehicleSize : '' , driverName : row . driverName === '—' ? '' : row . driverName , driverPhone : row . driverPhone === '—' ? '' : row . driverPhone , vehiclePlate : row . vehiclePlate === '—' ? '' : row . vehiclePlate , appointmentAccount : '' , sellerName : '' , deliveryFee : undefined , loadingFee : undefined , appointmentFee : undefined , cartonMarksJson : '' , remark : '' } ) ; mutationError . value = null ; transferOpen . value = true }
async function submitEdit ( ) { if ( ! editRow . value || ! canEdit ( editRow . value ) || editSaving . value ) return ; editSaving . value = true ; mutationError . value = null ; try { await updateDispatch ( editRow . value . dispatchNo , { vehiclePlate : editForm . vehiclePlate . trim ( ) || null , driverName : editForm . driverName . trim ( ) || null , driverPhone : editForm . driverPhone . trim ( ) || null , carrierName : editForm . carrierName . trim ( ) || null , route : editForm . route . trim ( ) || null , departAt : editForm . departAt || null , fbaAppointmentNo : editForm . fbaAppointmentNo . trim ( ) || null , fbaAppointmentStatus : editForm . fbaAppointmentStatus || null , vehicleAppointmentStatus : editForm . vehicleAppointmentStatus || null , estimatedArrivalAt : editForm . estimatedArrivalAt || null } ) ; editOpen . value = false ; ElMessage . success ( '排车信息已保存' ) ; await load ( ) } catch ( cause ) { mutationError . value = getApiErrorDetails ( cause , '排车信息保存失败,请重试' ) } finally { editSaving . value = false } }
async function cancel ( row : DispatchListItem ) { if ( ! canCancel ( row ) ) return ; try { await ElMessageBox . confirm ( ` 确认取消排车单 ${ row . dispatchNo } ?取消后将释放托盘锁。 ` , '取消排车' , { confirmButtonText : '确认取消' , cancelButtonText : '返回' , type : 'warning' } ) } catch { return } mutationError . value = null ; try { await cancelDispatch ( row . dispatchNo ) ; ElMessage . success ( '排车单已取消' ) ; await load ( ) } catch ( cause ) { mutationError . value = getApiErrorDetails ( cause , '排车取消失败,请重试' ) } }
function handleMore ( command : string , row : DispatchListItem ) { if ( command === 'edit' ) openEdit ( row ) ; else if ( command === 'cancel' ) void cancel ( row ) }
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'
}
function canTransfer ( row : DispatchListItem ) {
return access . can ( permission . dispatchTransferDelivery ) && [ 'PENDING_DEPARTURE' , 'PICKING' , 'READY_TO_DEPART' ] . includes ( row . statusCode )
}
function openEdit ( row : DispatchListItem ) {
if ( ! canEdit ( row ) ) return
const source = dispatchSnapshots . get ( row . dispatchNo )
editRow . value = row
editItems . value = [ ... ( source ? . items || [ ] ) ]
Object . assign ( editForm , {
vehiclePlate : source ? . vehiclePlate || '' ,
driverName : source ? . driverName || '' ,
driverPhone : source ? . driverPhone || '' ,
carrierName : source ? . carrierName || '' ,
route : source ? . route || '' ,
departAt : source ? . departAt || '' ,
fbaAppointmentNo : source ? . fbaAppointmentNo || '' ,
fbaAppointmentStatus : source ? . fbaAppointmentStatus || '' ,
vehicleAppointmentStatus : source ? . vehicleAppointmentStatus || '' ,
estimatedArrivalAt : source ? . estimatedArrivalAt || '' ,
} )
mutationError . value = null
editOpen . value = true
}
function openTransfer ( row : DispatchListItem ) {
if ( ! canTransfer ( row ) ) return
transferRow . value = row
Object . assign ( transferForm , {
plannedDeliveryAt : '' ,
deliveryMethod : '' ,
waybillNo : '' ,
carrierName : row . carrierName === '—' ? '' : row . carrierName ,
pickupMethod : '' ,
loadingType : '' ,
vehicleType : '' ,
vehicleSize : '' ,
driverName : row . driverName === '—' ? '' : row . driverName ,
driverPhone : row . driverPhone === '—' ? '' : row . driverPhone ,
vehiclePlate : row . vehiclePlate === '—' ? '' : row . vehiclePlate ,
appointmentAccount : '' ,
sellerName : '' ,
deliveryFee : undefined ,
loadingFee : undefined ,
appointmentFee : undefined ,
cartonMarksJson : '' ,
remark : '' ,
} )
mutationError . value = null
transferOpen . value = true
}
async function submitEdit ( ) {
if ( ! editRow . value || ! canEdit ( editRow . value ) || editSaving . value ) return
editSaving . value = true
mutationError . value = null
try {
await updateDispatch ( editRow . value . dispatchNo , {
vehiclePlate : editForm . vehiclePlate . trim ( ) || null ,
driverName : editForm . driverName . trim ( ) || null ,
driverPhone : editForm . driverPhone . trim ( ) || null ,
carrierName : editForm . carrierName . trim ( ) || null ,
route : editForm . route . trim ( ) || null ,
departAt : editForm . departAt || null ,
fbaAppointmentNo : editForm . fbaAppointmentNo . trim ( ) || null ,
fbaAppointmentStatus : editForm . fbaAppointmentStatus || null ,
vehicleAppointmentStatus : editForm . vehicleAppointmentStatus || null ,
estimatedArrivalAt : editForm . estimatedArrivalAt || null ,
} )
editOpen . value = false
ElMessage . success ( '排车信息已保存' )
await load ( )
} catch ( cause ) {
mutationError . value = getApiErrorDetails ( cause , '排车信息保存失败,请重试' )
} finally {
editSaving . value = false
}
}
async function removePallet ( itemValue : unknown ) {
const item = itemValue as DispatchItemDto
if ( ! editRow . value || removingItemId . value ) return
try {
await ElMessageBox . confirm ( ` 确认从排车单移除托盘 ${ text ( item . palletNo ) } ?移除后可重新排车。 ` , '移除排车托盘' , {
confirmButtonText : '确认移除' ,
cancelButtonText : '返回' ,
type : 'warning' ,
} )
} catch {
return
}
removingItemId . value = String ( item . id )
mutationError . value = null
try {
const updated = await removeDispatchItem ( editRow . value . dispatchNo , String ( item . id ) )
editItems . value = [ ... ( updated . items || [ ] ) ]
dispatchSnapshots . set ( editRow . value . dispatchNo , updated )
const row = rows . value . find ( ( value ) => value . dispatchNo === editRow . value ? . dispatchNo )
if ( row ) {
row . palletCount = number ( updated . palletCount )
row . packageCount = number ( updated . packageCount )
row . orderNos = text ( updated . orderNos )
}
ElMessage . success ( '托盘已移除,可重新排车' )
} catch ( cause ) {
mutationError . value = getApiErrorDetails ( cause , '托盘移除失败,请重试' )
} finally {
removingItemId . value = ''
}
}
async function cancel ( row : DispatchListItem ) {
if ( ! canCancel ( row ) ) return
try {
await ElMessageBox . confirm ( ` 确认取消排车单 ${ row . dispatchNo } ?取消后将释放托盘锁。 ` , '取消排车' , {
confirmButtonText : '确认取消' ,
cancelButtonText : '返回' ,
type : 'warning' ,
} )
} catch {
return
}
mutationError . value = null
try {
await cancelDispatch ( row . dispatchNo )
ElMessage . success ( '排车单已取消' )
await load ( )
} catch ( cause ) {
mutationError . value = getApiErrorDetails ( cause , '排车取消失败,请重试' )
}
}
function handleMore ( command : string , row : DispatchListItem ) {
if ( command === 'edit' ) openEdit ( row )
else if ( command === 'cancel' ) void cancel ( row )
}
async function submitTransfer ( ) {
if ( ! transferRow . value || ! canTransfer ( transferRow . value ) ) return
if ( ! transferForm . plannedDeliveryAt || ! transferForm . deliveryMethod || ! transferForm . pickupMethod || ! transferForm . loadingType ) { ElMessage . warning ( '请填写预计派送时间、派送方式、提货方式和装车类型' ) ; return }
if ( transferForm . deliveryFee == null ) { ElMessage . warning ( '请填写派送费用' ) ; return }
if ( needsLoadingFee . value && transferForm . loadingFee == null ) { ElMessage . warning ( '当前装车类型需要填写装车费' ) ; return }
if ( isBuyAppointment . value && ( ! transferForm . sellerName . trim ( ) || transferForm . appointmentFee == null ) ) { ElMessage . warning ( '买约时请填写卖家姓名和买约费用' ) ; return }
const feeInputs = [ [ 'DELIVERY_FEE' , transferForm . deliveryFee , '派送费用' ] , [ 'LOADING_FEE' , needsLoadingFee . value ? transferForm . loadingFee : undefined , '装车费' ] , [ 'APPOINTMENT_FEE' , isBuyAppointment . value ? transferForm . appointmentFee : undefined , '买约费用' ] ] as const
const fees = feeInputs . filter ( ( [ , amount ] ) => amount != null ) . map ( ( [ feeType , amount , feeName ] ) => ( { feeType , feeName , direction : 'RECEIVABLE' as const , currency : 'USD' as const , amount : Number ( amount ) , remark : feeName } ) )
if ( ! transferForm . plannedDeliveryAt || ! transferForm . deliveryMethod || ! transferForm . pickupMethod || ! transferForm . loadingType ) {
ElMessage . warning ( '请填写预计派送时间、派送方式、提货方式和装车类型' )
return
}
if ( transferForm . deliveryFee == null ) {
ElMessage . warning ( '请填写派送费用' )
return
}
if ( needsLoadingFee . value && transferForm . loadingFee == null ) {
ElMessage . warning ( '当前装车类型需要填写装车费' )
return
}
if ( isBuyAppointment . value && ( ! transferForm . sellerName . trim ( ) || transferForm . appointmentFee == null ) ) {
ElMessage . warning ( '买约时请填写卖家姓名和买约费用' )
return
}
const feeInputs = [
[ 'DELIVERY_FEE' , transferForm . deliveryFee , '派送费用' ] ,
[ 'LOADING_FEE' , needsLoadingFee . value ? transferForm . loadingFee : undefined , '装车费' ] ,
[ 'APPOINTMENT_FEE' , isBuyAppointment . value ? transferForm . appointmentFee : undefined , '买约费用' ] ,
] as const
const fees = feeInputs
. filter ( ( [ , amount ] ) => amount != null )
. map ( ( [ feeType , amount , feeName ] ) => ( {
feeType ,
feeName ,
direction : 'RECEIVABLE' as const ,
currency : 'USD' as const ,
amount : Number ( amount ) ,
remark : feeName ,
} ) )
let palletCartonMarks : Record < string , string [ ] > | undefined
if ( transferForm . cartonMarksJson . trim ( ) ) {
try { const parsed = JSON . parse ( transferForm . cartonMarksJson ) ; if ( ! parsed || typeof parsed !== 'object' || Array . isArray ( parsed ) ) throw new Error ( ) ; palletCartonMarks = Object . fromEntries ( Object . entries ( parsed ) . map ( ( [ pallet , marks ] ) => [ pallet , Array . isArray ( marks ) ? marks . map ( ( mark ) => String ( mark ) . trim ( ) ) . filter ( Boolean ) : [ ] ] ) ) } catch { ElMessage . warning ( '托盘箱唛分配格式不正确,请使用 JSON: {"托盘号":["箱唛1","箱唛2"]}' ) ; return }
}
transferSaving . value = true
try { await transferDispatchToDelivery ( transferRow . value . dispatchNo , { idempotencyKey : ` web-transfer- ${ transferRow . value . dispatchNo } - ${ Date . now ( ) } ` , plannedDeliveryAt : transferForm . plannedDeliveryAt , deliveryMethod : transferForm . deliveryMethod , waybillNo : transferForm . waybillNo . trim ( ) || undefined , carrierName : transferForm . carrierName . trim ( ) || undefined , pickupMethod : transferForm . pickupMethod , loadingType : transferForm . loadingType , vehicleType : transferForm . vehicleType || undefined , vehicleSize : transferForm . vehicleSize || undefined , driverName : transferForm . driverName . trim ( ) || undefined , driverPhone : transferForm . driverPhone . trim ( ) || undefined , vehiclePlate : transferForm . vehiclePlate . trim ( ) || undefined , appointmentAccount : transferForm . appointmentAccount || undefined , sellerName : isBuyAppointment . value ? transferForm . sellerName . trim ( ) : undefined , remark : transferForm . remark . trim ( ) || undefined , fees , palletCartonMarks } ) ; transferOpen . value = false ; ElMessage . success ( '已生成出库单和派送单' ) ; await load ( ) } catch ( cause ) { mutationError . value = getApiErrorDetails ( cause , '转派送失败,请重试' ) } finally { transferSaving . value = false }
try {
await transferDispatchToDelivery ( transferRow . value . dispatchNo , {
idempotencyKey : ` web-transfer- ${ transferRow . value . dispatchNo } - ${ Date . now ( ) } ` ,
plannedDeliveryAt : transferForm . plannedDeliveryAt ,
deliveryMethod : transferForm . deliveryMethod ,
waybillNo : transferForm . waybillNo . trim ( ) || undefined ,
carrierName : transferForm . carrierName . trim ( ) || undefined ,
pickupMethod : transferForm . pickupMethod ,
loadingType : transferForm . loadingType ,
vehicleType : transferForm . vehicleType || undefined ,
vehicleSize : transferForm . vehicleSize || undefined ,
driverName : transferForm . driverName . trim ( ) || undefined ,
driverPhone : transferForm . driverPhone . trim ( ) || undefined ,
vehiclePlate : transferForm . vehiclePlate . trim ( ) || undefined ,
appointmentAccount : transferForm . appointmentAccount || undefined ,
sellerName : isBuyAppointment . value ? transferForm . sellerName . trim ( ) : undefined ,
remark : transferForm . remark . trim ( ) || undefined ,
fees ,
palletCartonMarks ,
} )
transferOpen . value = false
ElMessage . success ( '已生成出库单和派送单' )
await load ( )
} catch ( cause ) {
mutationError . value = getApiErrorDetails ( cause , '转派送失败,请重试' )
} finally {
transferSaving . value = false
}
}
onMounted ( load )
@@ -243,6 +561,29 @@ onMounted(load)
< template # pagination -left > 共 { { total } } 条排车记录 < / template >
< / DataTable > < / section >
< / 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 >
< 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><h3>已排车托盘</h3><el-table :data="editItems" border max-height="280" empty-text="暂无托盘"><el-table-column label="操作" width="90" fixed="left"><template #default="{ row }"><el-button text type="danger" :loading="removingItemId === String(row.id)" :disabled="Boolean(removingItemId)" @click="removePallet(row)"><Trash2 :size="15" />移除</el-button></template></el-table-column><el-table-column prop="palletNo" label="托盘号" min-width="160" /><el-table-column prop="orderNo" label="订单号" min-width="160" /><el-table-column prop="inboundCode" label="入仓码" min-width="140" /><el-table-column prop="packageCount" label="箱数" width="80" align="right" / > < / el-table > < / AppDialog >
< / template >
< style scoped > . transfer - grid , . edit - grid { display : grid ; grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr ) ) ; gap : 0 16 px } . transfer - grid : deep ( . el - select ) , . transfer - grid : deep ( . el - date - editor ) , . transfer - grid : deep ( . el - input - number ) , . edit - grid : deep ( . el - select ) , . edit - grid : deep ( . el - date - editor ) { width : 100 % } h3 { margin : 4 px 0 12 px } @ media ( max - width : 900 px ) { . transfer - grid , . edit - grid { grid - template - columns : 1 fr } } < / style >
< style scoped >
. transfer - grid ,
. edit - grid {
display : grid ;
grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr ) ) ;
gap : 0 16 px ;
}
. transfer - grid : deep ( . el - select ) ,
. transfer - grid : deep ( . el - date - editor ) ,
. transfer - grid : deep ( . el - input - number ) ,
. edit - grid : deep ( . el - select ) ,
. edit - grid : deep ( . el - date - editor ) {
width : 100 % ;
}
h3 {
margin : 4 px 0 12 px ;
}
@ media ( max - width : 900 px ) {
. transfer - grid ,
. edit - grid {
grid - template - columns : 1 fr ;
}
}
< / style >