2026-10-03 04:35:55 +08:00
< script setup lang = "ts" >
import '@/styles/adminDensity.css'
2026-10-15 17:11:03 +08:00
import { appointmentReturnTarget , appointmentReturnQuery } from '@/modules/appointment-calendar'
2026-10-03 04:35:55 +08:00
import { onBeforeUnmount , onMounted , ref , watch } from 'vue'
2026-10-15 17:11:03 +08:00
import { useRoute , useRouter } from 'vue-router'
2026-10-03 04:35:55 +08:00
import AdminPage from '@/shared/components/AdminPage.vue'
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
import DataTable , { type DataTableColumn } from '@/shared/components/DataTable.vue'
import QueryToolbar , { type QueryField , type QueryModel } from '@/shared/components/QueryToolbar.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import TripPlanDialog from '../components/TripPlanDialog.vue'
2026-10-19 10:23:38 +08:00
import PickupDemandPanel from '../components/PickupDemandPanel.vue'
2026-10-03 04:35:55 +08:00
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
import { listTrips , type TripQuery } from '../api/trips'
import { overseasMasterDataPermissions as permission } from '../permissions'
import { tripStatusLabels , type FleetTrip , type TripDetail } from '../tripTypes'
2026-10-15 17:11:03 +08:00
const route = useRoute (), router = useRouter (), access = useAccessPreviewStore ()
2026-10-19 10:23:38 +08:00
const workTab = ref ( 'trips' ), pickupVisited = ref ( false )
watch ( workTab , value => { if ( value === 'pickups' ) pickupVisited . value = true })
2026-10-03 04:35:55 +08:00
const rows = ref < FleetTrip [] >([]), total = ref ( 0 ), page = ref ( 1 ), size = ref ( 20 ), loading = ref ( false ), editorOpen = ref ( false )
const error = ref < ApiErrorDetails | null >( null ), filters = ref < QueryModel >({ keyword : '' , status : '' })
let applied : Omit < TripQuery , ' page ' | ' size ' > = {}, sequence = 0
const fields : QueryField [] = [{ key : 'keyword' , type : 'input' , label : '关键词' , placeholder : '车次号 / 车牌 / 司机' , width : 250 },
2026-10-07 12:24:21 +08:00
{ key : 'status' , type : 'select' , label : '状态' , placeholder : '全部状态' , width : 150 , options : Object . entries ( tripStatusLabels ). map (([ value , label ]) => ({ value , label })) }]
2026-10-03 04:35:55 +08:00
const columns : DataTableColumn [] = [
2026-10-03 05:02:39 +08:00
{ key : 'actions' , label : '操作' , width : 85 , fixed : 'left' , slotName : 'actions' }, { key : 'tripNo' , label : '车次号' , minWidth : 215 },
2026-10-19 10:23:38 +08:00
{ key : 'purpose' , label : '任务类型' , slotName : 'purpose' , width : 110 }, { key : 'status' , label : '状态' , slotName : 'status' , width : 120 }, { key : 'warehouseName' , label : '起运仓' , minWidth : 160 },
2026-10-03 05:02:39 +08:00
{ key : 'vehiclePlate' , label : '车牌' , minWidth : 130 }, { key : 'driverName' , label : '司机' , minWidth : 110 },
2026-10-03 04:35:55 +08:00
{ key : 'departure' , label : '计划发车' , slotName : 'departure' , minWidth : 175 }, { key : 'end' , label : '司机预计结束' , slotName : 'end' , minWidth : 175 },
2026-10-03 05:02:39 +08:00
{ key : 'route' , label : '路线' , slotName : 'route' , minWidth : 240 }, { key : 'remark' , label : '备注' , minWidth : 180 },
2026-10-03 04:35:55 +08:00
]
const time = ( value : string ) => value ? new Date ( value ). toLocaleString ( 'zh-CN' , { hour12 : false }) : '—'
async function load () {
const current = ++ sequence ; loading . value = true ; error . value = null
try { const result = await listTrips ({ ... applied , page : page . value , size : size . value }); if ( current === sequence ) { rows . value = result . records ; total . value = result . total } }
catch ( cause ) { if ( current === sequence ) { rows . value = []; total . value = 0 ; error . value = getApiErrorDetails ( cause , '车次加载失败' ) } }
finally { if ( current === sequence ) loading . value = false }
}
function search () {
applied = { keyword : String ( filters . value . keyword || '' ). trim () || undefined , status : ( filters . value . status || undefined ) as TripQuery [ 'status' ] }
if ( page . value !== 1 ) page . value = 1 ; else void load ()
}
2026-10-15 17:11:03 +08:00
function view ( tripNo : string ) { void router . push ({ name : 'overseas-master-data-trip-detail' , params : { tripNo }, query : appointmentReturnQuery ( route . query ) }) }
2026-10-03 04:35:55 +08:00
function created ( detail : TripDetail ) { view ( detail . trip . tripNo ) }
watch ([ page , size ], load ); onMounted ( load ); onBeforeUnmount (() => { sequence ++ })
</ script >
< template >
2026-10-07 12:24:21 +08:00
< AdminPage class = "admin-density" >< ListPageLayout aria -label =" 车次管理 " >
2026-10-19 10:23:38 +08:00
< el-tabs v-model = "workTab" class="density-element-tabs"><el-tab-pane label="车次管理" name="trips" /><el-tab-pane label="待取货" name="pickups" /></ el-tabs >
< PickupDemandPanel v-if = "pickupVisited" v-show="workTab === 'pickups'" />
< DataTable v-show = "workTab === 'trips'" appearance="integrated" :stripe="false" column-settings-mode="show" :results-visible="true" preference-key="fleet-trips-table" v-model:page="page" v-model:page-size="size" :data="rows" :columns="columns" :total="total" :loading="loading" row-key="tripNo" empty-text="暂无车次,先建立计划再关联排车单" >
2026-10-15 17:11:03 +08:00
< template # toolbar -start >< el-button v-if = "appointmentReturnTarget(route.query)" link @click="router.push(appointmentReturnTarget(route.query)!)" > 返回预约工作 </ el -button >< h1 > 车次管理 </ h1 ></ template >
2026-10-07 12:24:21 +08:00
< template # filters >< QueryToolbar v-model = "filters" :fields="fields" ariaLabel="车次查询" preference-key="fleet-trips-query" :loading="loading" @search="search" @reset="search" /></ template >
< template # notices >< LoadErrorState v-if = "error" title="车次加载失败" :message="error.message" @retry="load"><template #details><ApiErrorMeta :details="error" /></ template ></ LoadErrorState ></ template >
2026-10-03 04:35:55 +08:00
< template # toolbar -actions >< el-button v-if = "access.can(permission.tripCreate)" type="primary" @click="editorOpen = true" > 新建车次 </ el -button ></ template >
2026-10-07 12:24:21 +08:00
< template # actions = "{ row }" >< div class = "density-actions row-actions admin-text-actions" >< el-button class = "density-action" @click ="view(row.tripNo)" > 详情 </ el -button ></ div ></ template >
2026-10-19 10:23:38 +08:00
< template # purpose = "{ row }" >{{ row . tripPurpose === 'PICKUP' ? '入仓取货' : '出库派送' }}</ template >
2026-10-03 04:35:55 +08:00
< template # status = "{ row }" >{{ tripStatusLabels [ row . status as keyof typeof tripStatusLabels ] }}</ template >
< template # departure = "{ row }" >{{ time ( row . plannedDepartureAt ) }}</ template >< template # end = "{ row }" >{{ time ( row . expectedDriverEndAt ) }}</ template >
< template # route = "{ row }" >{{ row . stops . map (( stop : FleetTrip [ 'stops' ][ number ]) => stop . name ). join ( ' → ' ) }}</ template >
</ DataTable >
< TripPlanDialog v-model = "editorOpen" :trip="null" @saved="created" />
</ ListPageLayout ></ AdminPage >
</ template >