2026-08-08 13:32:27 +08:00
< script setup lang = "ts" >
2026-08-08 15:07:34 +08:00
import { onMounted , reactive , ref , watch } from 'vue'
2026-08-08 13:32:27 +08:00
import { useRouter } from 'vue-router'
import QueryToolbar , { type QueryField , type QueryModel } from '@/shared/components/QueryToolbar.vue'
2026-08-08 15:07:34 +08:00
import DataTable from '@/shared/components/DataTable.vue'
2026-08-08 15:05:37 +08:00
import StatusBadge from '@/shared/components/StatusBadge.vue'
2026-08-08 15:07:34 +08:00
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import { getApiErrorDetails } from '@/shared/api/http'
2026-08-08 15:05:37 +08:00
import { useAccessPreviewStore } from '@/core/access/accessPreview'
2026-08-08 13:32:27 +08:00
import { listOceanBookings , type OceanBookingResource } from '../api/platformOceanBookings'
import type { OceanBookingQuery , OceanBookingStatus } from '../types'
2026-08-08 15:05:37 +08:00
const router = useRouter ()
const access = useAccessPreviewStore ()
const rows = ref < OceanBookingResource [] >([])
const loading = ref ( false )
2026-08-08 15:07:34 +08:00
const error = ref < ReturnType < typeof getApiErrorDetails > | null > ( null )
2026-08-08 15:05:37 +08:00
const page = ref ( 1 )
const pageSize = ref ( 20 )
const total = ref ( 0 )
2026-08-10 09:12:06 +08:00
const query = reactive < QueryModel >({ keyword : '' , status : '' })
2026-08-08 15:07:34 +08:00
const statuses : Array < { label : string ; value : string } > = [
{ label : '草稿' , value : 'DRAFT' }, { label : '已提交' , value : 'SUBMITTED' }, { label : '已确认' , value : 'CONFIRMED' },
{ label : '已放舱' , value : 'RELEASED' }, { label : '失败' , value : 'FAILED' }, { label : '变更中' , value : 'CHANGE_PENDING' }, { label : '已取消' , value : 'CANCELLED' },
2026-08-08 15:05:37 +08:00
]
2026-08-08 15:07:34 +08:00
const fields : QueryField [] = [
{ key : 'keyword' , type : 'input' , label : '关键词' , placeholder : '订舱号/订单号/客户' , width : 220 },
{ key : 'status' , type : 'select' , label : '状态' , options : statuses },
2026-08-08 15:05:37 +08:00
]
2026-08-08 15:07:34 +08:00
const columns = [
{ key : 'actions' , label : '操作' , fixed : 'left' , width : 170 , slotName : 'actions' , hideable : false , movable : false },
2026-08-10 09:12:06 +08:00
{ prop : 'bookingNo' , label : '订舱编号' , minWidth : 150 }, { prop : 'customerName' , label : '客户' , minWidth : 160 },
{ prop : 'billOfLadingNo' , label : '提单号' , minWidth : 140 }, { prop : 'pol' , label : '起运港' , minWidth : 125 }, { prop : 'pod' , label : '目的港' , minWidth : 125 },
{ prop : 'carrierName' , label : '船公司' , minWidth : 130 }, { prop : 'bookingAgent' , label : '订舱代理' , minWidth : 140 },
{ prop : 'vesselName' , label : '船名/航次' , minWidth : 150 , slotName : 'vessel' }, { prop : 'etd' , label : '预计开船' , minWidth : 140 },
{ prop : 'eta' , label : '预计到港' , minWidth : 140 }, { prop : 'status' , label : '状态' , minWidth : 110 , slotName : 'status' },
2026-08-08 15:07:34 +08:00
]
2026-08-10 09:12:06 +08:00
async function load () { loading . value = true ; error . value = null ; try { const result = await listOceanBookings ({ page : page . value , size : pageSize . value , keyword : String ( query . keyword || '' ) || undefined , status : ( query . status || undefined ) as OceanBookingStatus | undefined } satisfies OceanBookingQuery ); rows . value = result . records ; total . value = result . total } catch ( e ) { error . value = getApiErrorDetails ( e ) } finally { loading . value = false } }
2026-08-08 15:05:37 +08:00
function open ( id ?: string ) { void router . push ( id ? `/ocean-bookings/ ${ id } ` : '/ocean-bookings/new' ) }
2026-08-08 15:07:34 +08:00
function search () { page . value = 1 ; void load () }
watch ([ page , pageSize ], () => void load ()); onMounted ( load )
2026-08-08 13:32:27 +08:00
</ script >
2026-08-08 15:05:37 +08:00
< template >
2026-08-08 15:07:34 +08:00
< div class = "business-page ocean-bookings-page" >
< QueryToolbar v-model = "query" ariaLabel="订舱筛选" :fields="fields" @search="search" />
< LoadErrorState v-if = "error" title="订舱列表加载失败" :message="error.message" @retry="load" />
< section v-else 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 =" ocean -bookings " aria -label =" 订舱列表 " row -key =" id " empty -text =" 暂无符合筛选条件的订舱 " >
2026-08-08 15:05:37 +08:00
< template # toolbar -start >< div class = "table-heading" >< h2 > 订舱列表 </ h2 >< span > 共 {{ total }} 条 </ span ></ div ></ template >
2026-08-08 15:07:34 +08:00
< template # toolbar -actions >< el-button v-if = "access.can('platform:firstleg:ocean-booking:create')" type="primary" @click="open()" > 新增订舱 </ el -button ></ template >
< template # actions = "{ row }" >< el-button v-if = "access.can('platform:firstleg:ocean-booking:view')" link type="primary" @click="open(row.id)">查看</el-button><el-button v-if="access.can('platform:firstleg:ocean-booking:edit')" link @click="open(row.id)" > 编辑 </ el -button ></ template >
< template # vessel = "{ row }" >{{ row . vesselName || '—' }}< span v-if = "row.voyageNo" > / {{ row.voyageNo }}</ span ></ template >
2026-08-08 15:05:37 +08:00
< template # status = "{ row }" >< StatusBadge :status = "row.status" /></ template >
</ DataTable >
</ section >
2026-08-08 15:07:34 +08:00
</ div >
2026-08-08 15:05:37 +08:00
</ template >
2026-08-08 15:07:34 +08:00
< style scoped >. ocean - bookings - page { display : grid ; gap : 16 px }. table - heading { display : flex ; align - items : baseline ; gap : 10 px }. table - heading h2 { margin : 0 ; font - size : 16 px }. table - heading span { color : var ( -- cc - muted ); font - size : 12 px }</ style >