2026-08-19 16:45:51 +08:00
< script setup lang = "ts" >
2026-09-15 09:58:15 +08:00
import '@/styles/adminDensity.css'
2026-10-11 15:33:09 +08:00
import { inquiryReturnTarget , TransportRouteDialog , routeLocation , type RouteLocation } from '@/modules/overseas-master-data'
2026-08-20 09:10:36 +08:00
import { computed , onMounted , ref , watch } from 'vue'
2026-10-09 16:11:59 +08:00
import { useRoute , useRouter } from 'vue-router'
2026-09-15 09:58:15 +08:00
import { Plus } from '@lucide/vue'
2026-08-19 16:45:51 +08:00
import AdminPage from '@/shared/components/AdminPage.vue'
2026-08-20 09:10:36 +08:00
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
2026-08-19 16:45:51 +08:00
import DataTable , { type DataTableColumn } from '@/shared/components/DataTable.vue'
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
2026-08-20 09:10:36 +08:00
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
2026-08-19 16:45:51 +08:00
import QueryToolbar , { type QueryField , type QueryModel } from '@/shared/components/QueryToolbar.vue'
2026-10-07 16:46:57 +08:00
import AppDialog from '@/shared/components/AppDialog.vue'
import FleetArrangementSummary from '../components/FleetArrangementSummary.vue'
import { poolOrderStatusLabels , type FleetArrangement } from '../fleetArrangement'
2026-08-20 09:10:36 +08:00
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
2026-08-19 16:45:51 +08:00
import { useAccessPreviewStore } from '@/core/access/accessPreview'
2026-08-20 18:02:52 +08:00
import { listLocalLooseboardOrders , listUsTruckOrders } from '../api/platformOverseasBusiness'
2026-08-19 16:45:51 +08:00
import { overseasBusinessPermissions } from '../permissions'
2026-10-19 15:11:15 +08:00
import type { LocalLooseboardOrder , LocalLooseboardQuery , LocalLooseboardStatus , LocalTransportOrder , LocalTransportQuery , LocalTransportStatus , LocalTransportType } from '../types'
2026-08-19 16:45:51 +08:00
const props = defineProps < { domain : 'truck' | 'looseboard' } > ()
2026-10-09 17:36:32 +08:00
const routeOpen = ref ( false )
const routeOrigin = ref < RouteLocation >( routeLocation ( '' )), routeDestinations = ref < RouteLocation [] >([])
function openRoute ( row : LocalTransportOrder | LocalLooseboardOrder ) {
routeOrigin . value = routeLocation ( row . origin )
routeDestinations . value = [ routeLocation ( row . destination )]
routeOpen . value = true
}
2026-10-09 16:11:59 +08:00
const route = useRoute (); const router = useRouter (); const access = useAccessPreviewStore ()
2026-08-20 09:10:36 +08:00
const isLooseboard = computed (() => props . domain === 'looseboard' ); const domainLabel = computed (() => isLooseboard . value ? '本土散板' : '本土卡车' )
const queryPreferenceKey = computed (() => isLooseboard . value ? 'overseas-business-looseboard-query' : 'overseas-business-truck-query' )
2026-08-19 16:45:51 +08:00
const createPermission = computed (() => isLooseboard . value ? overseasBusinessPermissions . looseboardCreate : overseasBusinessPermissions . truckCreate )
2026-08-20 09:10:36 +08:00
const editPermission = computed (() => isLooseboard . value ? overseasBusinessPermissions . looseboardEdit : overseasBusinessPermissions . truckEdit )
const rows = ref < Array < LocalTransportOrder | LocalLooseboardOrder > > ([]); const loading = ref ( false ); const loadError = ref < ApiErrorDetails | null >( null )
const page = ref ( 1 ); const pageSize = ref ( 20 ); const total = ref ( 0 )
2026-10-07 16:46:57 +08:00
const arrangementOpen = ref ( false ), arrangement = ref < FleetArrangement | null >( null )
function showArrangement ( row : LocalTransportOrder | LocalLooseboardOrder ) { arrangement . value = row . fleetArrangement ; arrangementOpen . value = true }
2026-10-09 16:11:59 +08:00
const filters = ref < QueryModel >({ orderNo : String ( route . query . orderNo || '' ), status : '' , routeKeyword : '' }); const applied = ref ({ ... filters . value })
2026-08-20 09:10:36 +08:00
const truckStatusLabels : Record < LocalTransportStatus , string > = { PENDING_REVIEW : '待审核' , WAITING_INBOUND : '待入仓' , RECEIVING : '收货中' , WAITING_CONSOLIDATION : '待合托' , PALLET_OVERAGE_REVIEW : '超托待审' , PARTIALLY_ALLOCATED : '部分分配' , ALLOCATED : '已分配' , PENDING_DISPATCH : '待派车' , DISPATCHED : '已派车' , IN_TRANSIT : '运输中' , COMPLETED : '已完成' , REJECTED : '已拒绝' , CANCELLED : '已取消' }
2026-10-07 16:46:57 +08:00
const looseboardStatusLabels : Record < LocalLooseboardStatus , string > = { PENDING_ACCEPTANCE : '待接单' , WAITING_INBOUND : '待入仓' , RECEIVING : '收货中' , WAITING_CONSOLIDATION : '待合托' , PENDING_DISPATCH : '待派车' , DISPATCHED : '已派车' , IN_TRANSIT : '运输中' , COMPLETED : '已完成' , CANCELLED : '已取消' , ... poolOrderStatusLabels }
2026-08-20 09:10:36 +08:00
const transportTypeLabels : Record < LocalTransportType , string > = { PORT_PICKUP : '港口提柜' , WAREHOUSE_TO_WAREHOUSE : '仓到仓' , WAREHOUSE_TO_DOOR : '仓到门' }
const fields = computed < QueryField [] >(() => [
2026-10-09 16:11:59 +08:00
{ key : 'orderNo' , type : 'input' , label : '商业单号' , placeholder : '请输入商业单号' , width : 230 },
2026-08-20 18:02:52 +08:00
{ key : 'status' , type : 'select' , label : '状态' , placeholder : '全部状态' , width : 170 , options : Object . entries ( isLooseboard . value ? looseboardStatusLabels : truckStatusLabels ). map (([ value , label ]) => ({ value , label })) },
2026-08-19 16:45:51 +08:00
{ key : 'routeKeyword' , type : 'input' , label : '运输路线' , placeholder : '起运地/目的地' , width : 240 },
2026-08-20 09:10:36 +08:00
])
const truckColumns : DataTableColumn [] = [
2026-08-20 18:02:52 +08:00
{ key : 'actions' , label : '操作' , width : 110 , fixed : 'left' , slotName : 'actions' }, { key : 'orderNo' , prop : 'orderNo' , label : '订单编号' , minWidth : 170 },
2026-10-19 14:38:47 +08:00
{ key : 'vehicleType' , label : '车辆类型' , minWidth : 110 , formatter : ( row : LocalTransportOrder ) => row . vehicleType ? ({ PALLET_15 : '15拖' , PALLET_30 : '30拖' })[ row . vehicleType ] : '—' },
2026-08-20 09:10:36 +08:00
{ key : 'origin' , prop : 'origin' , label : '起运地' , minWidth : 145 }, { key : 'destination' , prop : 'destination' , label : '目的地' , minWidth : 145 },
2026-10-19 14:38:47 +08:00
{ key : 'originContactName' , prop : 'originContactName' , label : '起运地联系人' , minWidth : 125 }, { key : 'originContactPhone' , prop : 'originContactPhone' , label : '起运地联系电话' , minWidth : 155 },
{ key : 'destinationContactName' , prop : 'destinationContactName' , label : '目的地联系人' , minWidth : 125 }, { key : 'destinationContactPhone' , prop : 'destinationContactPhone' , label : '目的地联系电话' , minWidth : 155 },
2026-08-20 09:10:36 +08:00
{ key : 'packageCount' , prop : 'packageCount' , label : '件数' , width : 80 , align : 'right' }, { key : 'totalWeightKg' , prop : 'totalWeightKg' , label : '总重量(kg)' , width : 115 , align : 'right' },
{ key : 'budgetAmount' , label : '预算总价' , width : 120 , align : 'right' , formatter : ( row : LocalTransportOrder ) => row . budgetAmount == null ? '—' : ` ${ row . budgetAmount } ${ row . currency } ` },
2026-10-19 14:38:47 +08:00
{ key : 'pickupAt' , label : '取货时间' , width : 165 , formatter : ( row : LocalTransportOrder ) => formatDateTime ( row . pickupAt ) },
{ key : 'deliveryAt' , label : '送货时间' , width : 165 , formatter : ( row : LocalTransportOrder ) => formatDateTime ( row . deliveryAt ) }, { key : 'status' , label : '状态' , width : 120 , formatter : ( row : LocalTransportOrder ) => truckStatusLabels [ row . status ] },
2026-08-28 14:44:53 +08:00
{ key : 'createdAt' , prop : 'createdAt' , label : '创建时间' , minWidth : 165 , formatter : ( row : LocalTransportOrder ) => formatDateTime ( row . createdAt ) },
2026-08-20 09:10:36 +08:00
]
const looseboardColumns : DataTableColumn [] = [
{ key : 'actions' , label : '操作' , width : 110 , fixed : 'left' , slotName : 'actions' }, { key : 'orderNo' , prop : 'orderNo' , label : '订单编号' , minWidth : 170 },
2026-10-19 15:11:15 +08:00
{ key : 'transportType' , label : '运输类型' , minWidth : 120 , formatter : ( row : LocalLooseboardOrder ) => row . transportType ? transportTypeLabels [ row . transportType ] : '—' }, { key : 'origin' , prop : 'origin' , label : '起运地' , minWidth : 145 },
{ key : 'destination' , prop : 'destination' , label : '目的地' , minWidth : 145 }, { key : 'originContactName' , prop : 'originContactName' , label : '起运地联系人' , minWidth : 125 }, { key : 'originContactPhone' , prop : 'originContactPhone' , label : '起运地联系电话' , minWidth : 155 },
{ key : 'destinationContactName' , prop : 'destinationContactName' , label : '目的地联系人' , minWidth : 125 }, { key : 'destinationContactPhone' , prop : 'destinationContactPhone' , label : '目的地联系电话' , minWidth : 155 }, { key : 'packageCount' , prop : 'packageCount' , label : '件数' , width : 80 , align : 'right' },
2026-08-20 18:02:52 +08:00
{ key : 'totalWeightKg' , prop : 'totalWeightKg' , label : '总重量(kg)' , width : 115 , align : 'right' }, { key : 'budgetAmount' , label : '预算总价' , width : 115 , align : 'right' , formatter : ( row : LocalLooseboardOrder ) => row . budgetAmount == null ? '—' : ` ${ row . budgetAmount } ${ row . currency } ` },
2026-10-19 15:11:15 +08:00
{ key : 'productName' , prop : 'productName' , label : '商品名称' , minWidth : 150 }, { key : 'estimatedPalletCount' , prop : 'estimatedPalletCount' , label : '预计托数' , width : 100 }, { key : 'pickupAt' , label : '取货时间' , width : 165 , formatter : ( row : LocalLooseboardOrder ) => formatDateTime ( row . pickupAt ) }, { key : 'deliveryAt' , label : '送货时间' , width : 165 , formatter : ( row : LocalLooseboardOrder ) => formatDateTime ( row . deliveryAt ) }, { key : 'status' , label : '状态' , width : 105 , formatter : ( row : LocalLooseboardOrder ) => looseboardStatusLabels [ row . status ] },
2026-08-28 14:44:53 +08:00
{ key : 'createdAt' , prop : 'createdAt' , label : '创建时间' , minWidth : 165 , formatter : ( row : LocalLooseboardOrder ) => formatDateTime ( row . createdAt ) },
2026-08-19 16:45:51 +08:00
]
2026-10-09 17:36:32 +08:00
const columns = computed (() => {
if ( ! isLooseboard . value ) return truckColumns
const visible = [... looseboardColumns ]
visible . splice ( visible . findIndex ( column => column . key === 'status' ), 0 ,
{ key : 'routeMap' , label : '路线地图' , width : 120 , slotName : 'routeMap' })
return visible
})
2026-08-28 14:44:53 +08:00
function formatDateTime ( value ?: string | null ) { return value ? value . replace ( 'T' , ' ' ). slice ( 0 , 16 ) : '—' }
2026-08-20 09:10:36 +08:00
async function load () {
2026-10-11 15:33:09 +08:00
const sourceBooking = route . query . fleetBookingNo
const sourceOrder = route . query . orderNo
arrangementOpen . value = false ; arrangement . value = null
2026-08-20 18:02:52 +08:00
rows . value = []; total . value = 0 ; loadError . value = null ; loading . value = true
try {
if ( isLooseboard . value ) {
2026-10-09 16:11:59 +08:00
const result = await listLocalLooseboardOrders ({ page : page . value , size : pageSize . value , orderNo : String ( applied . value . orderNo || '' ), routeKeyword : String ( applied . value . routeKeyword || '' ), status : String ( applied . value . status || '' ) as LocalLooseboardStatus | '' } satisfies LocalLooseboardQuery )
2026-08-20 18:02:52 +08:00
rows . value = result . records ; total . value = result . total
} else {
2026-10-09 16:11:59 +08:00
const result = await listUsTruckOrders ({ page : page . value , size : pageSize . value , orderNo : String ( applied . value . orderNo || '' ), routeKeyword : String ( applied . value . routeKeyword || '' ), status : String ( applied . value . status || '' ) as LocalTransportStatus | '' } satisfies LocalTransportQuery )
2026-08-20 18:02:52 +08:00
rows . value = result . records ; total . value = result . total
}
2026-10-11 15:33:09 +08:00
if ( inquiryReturnTarget ( route . query ) && route . query . fleetBookingNo === sourceBooking && route . query . orderNo === sourceOrder ) {
const target = rows . value . find ( row => row . orderNo === sourceOrder && row . fleetArrangement ? . bookingNo === sourceBooking )
if ( target ) showArrangement ( target )
}
2026-08-20 18:02:52 +08:00
} catch ( error ) { loadError . value = getApiErrorDetails ( error , ` ${ domainLabel . value } 订单加载失败,请稍后重试` ) } finally { loading . value = false }
2026-08-20 09:10:36 +08:00
}
function search ( model : QueryModel ) { applied . value = { ... model }; if ( page . value === 1 ) void load (); else page . value = 1 }
2026-08-20 18:02:52 +08:00
function openCreate () { void router . push ({ name : isLooseboard . value ? 'overseas-business-looseboard-new' : 'overseas-business-truck-new' }) }
2026-10-07 16:46:57 +08:00
function canEdit ( row : LocalTransportOrder | LocalLooseboardOrder ) { return ! row . fleetArrangement && ( isLooseboard . value ? row . status === 'PENDING_ACCEPTANCE' : row . status === 'PENDING_REVIEW' ) }
2026-08-20 18:02:52 +08:00
function openEdit ( row : LocalTransportOrder | LocalLooseboardOrder ) { if ( ! canEdit ( row )) return ; void router . push ({ name : isLooseboard . value ? 'overseas-business-looseboard-edit' : 'overseas-business-truck-edit' , params : { orderId : row . orderNo }, query : { customerId : row . customerId } }) }
2026-10-09 16:11:59 +08:00
watch ([ page , pageSize ], () => { void load () }); watch ([ isLooseboard , () => route . query . orderNo ], () => { page . value = 1 ; applied . value = { orderNo : String ( route . query . orderNo || '' ), status : '' , routeKeyword : '' }; filters . value = { ... applied . value }; void load () }); onMounted (() => { void load () })
2026-08-19 16:45:51 +08:00
</ script >
< template >
2026-09-15 09:58:15 +08:00
< AdminPage class = "admin-density" >< ListPageLayout :aria-label = "`${domainLabel}订单列表`" >
2026-09-30 14:01:58 +08:00
< section >< DataTable appearance = "integrated" :stripe = "false" column -settings-mode =" show " :results-visible = "!loadError" v -model:page = "page" v -model:page-size = "pageSize" :data = "rows" :columns = "columns" :loading = "loading" :total = "total" :preference-key = "`overseas-business-${domain}`" row -key =" id " :empty-text = "`暂无符合筛选条件的${domainLabel}订单`" >
2026-10-11 15:33:09 +08:00
< template # toolbar -start >< el-button v-if = "inquiryReturnTarget(route.query)" link @click="router.push(inquiryReturnTarget(route.query)!)" > 返回原拼车登记 </ el -button >< h1 >{{ domainLabel }}</ h1 ></ template >
2026-09-30 14:01:58 +08:00
< template # filters >< QueryToolbar v-model = "filters" :fields="fields" :ariaLabel="`${domainLabel}订单筛选`" :preference-key="queryPreferenceKey" @search="search" /></ template >
< template # notices >< template v-if = "loadError"><LoadErrorState :title="`${domainLabel}订单暂时无法加载`" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></ template ></ LoadErrorState ></ template ></ template >
2026-08-20 18:02:52 +08:00
< template # toolbar -actions >< el-button v-if = "access.can(createPermission)" type="primary" @click="openCreate"><Plus :size="16" /> 新增订单 </ el-button ></ template >
2026-10-07 16:46:57 +08:00
< template # actions = "{ row }" >< div class = "row-actions density-actions admin-text-actions" >< el-button v-if = "row.fleetArrangement" class="density-action" @click="showArrangement(row as LocalTransportOrder | LocalLooseboardOrder)">承运安排</el-button><el-tooltip v-if="!row.fleetArrangement && access.can(editPermission)" :disabled="canEdit(row as LocalTransportOrder | LocalLooseboardOrder)" content="当前状态不可编辑"><span><el-button class="density-action" data-tone="neutral" size="small" :disabled="!canEdit(row as LocalTransportOrder | LocalLooseboardOrder)" @click="openEdit(row as LocalTransportOrder | LocalLooseboardOrder)" > 编辑 </ el -button ></ span ></ el-tooltip ></ div ></ template >
2026-08-20 09:10:36 +08:00
< template # pagination -left > 共 {{ total }} 条 {{ domainLabel }} 订单 </ template >
2026-10-09 17:36:32 +08:00
< template # routeMap = "{ row }" >< el-button v-if = "row.orderNo" link type="primary" @click="openRoute(row as LocalLooseboardOrder)" > 查看路线 </ el -button ></ template >
2026-08-19 16:45:51 +08:00
</ DataTable ></ section >
2026-10-09 17:36:32 +08:00
</ ListPageLayout >< AppDialog v-model = "arrangementOpen" class="admin-density-dialog" title="承运安排" width="1080px" :show-confirm="false" cancel-text="关闭"><FleetArrangementSummary v-if="arrangement" :arrangement="arrangement" /></AppDialog><TransportRouteDialog v-model="routeOpen" :origin="routeOrigin" :destinations="routeDestinations" /></ AdminPage >
2026-08-19 16:45:51 +08:00
</ template >
2026-08-20 18:02:52 +08:00
< style scoped >. table - panel { overflow : hidden ; }</ style >