158 lines
16 KiB
Vue
158 lines
16 KiB
Vue
<script setup lang="ts">
|
|
import '@/styles/adminWorkbench.css'
|
|
import '@/styles/adminDensity.css'
|
|
import { inquiryReturnTarget, TransportRouteDialog, routeLocation, type RouteLocation } from '@/modules/overseas-master-data'
|
|
import { computed, onActivated, onMounted, ref, watch } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { Plus } from '@lucide/vue'
|
|
import AdminPage from '@/shared/components/AdminPage.vue'
|
|
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
|
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
|
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
|
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
|
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
|
import AppDialog from '@/shared/components/AppDialog.vue'
|
|
import RowActions, { type RowActionItem } from '@/shared/components/RowActions.vue'
|
|
import { truckStatusLabels } from '../truckWorkflow'
|
|
import FleetArrangementSummary from '../components/FleetArrangementSummary.vue'
|
|
import { poolOrderStatusLabels, type FleetArrangement } from '../fleetArrangement'
|
|
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
|
import { listLocalLooseboardOrders, listUsTruckOrders } from '../api/platformOverseasBusiness'
|
|
import { overseasBusinessPermissions } from '../permissions'
|
|
import type { LocalLooseboardOrder, LocalLooseboardQuery, LocalLooseboardStatus, LocalTransportOrder, LocalTransportQuery, LocalTransportStatus, LocalTransportType } from '../types'
|
|
|
|
const props = defineProps<{ domain: 'truck' | 'looseboard' }>()
|
|
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
|
|
}
|
|
const route = useRoute(); const router = useRouter(); const access = useAccessPreviewStore()
|
|
const isLooseboard = computed(() => props.domain === 'looseboard'); const domainLabel = computed(() => isLooseboard.value ? '本土散板' : '本土卡车')
|
|
const queryPreferenceKey = computed(() => isLooseboard.value ? 'overseas-business-looseboard-query' : 'overseas-business-truck-query')
|
|
const createPermission = computed(() => isLooseboard.value ? overseasBusinessPermissions.looseboardCreate : overseasBusinessPermissions.truckCreate)
|
|
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)
|
|
const arrangementOpen = ref(false), arrangement = ref<FleetArrangement | null>(null)
|
|
function showArrangement(row: LocalTransportOrder | LocalLooseboardOrder) { arrangement.value = row.fleetArrangement; arrangementOpen.value = true }
|
|
const filters = ref<QueryModel>({ orderNo: String(route.query.orderNo || ''), status: '', routeKeyword: '' }); const applied = ref({ ...filters.value })
|
|
const looseboardStatusLabels: Record<LocalLooseboardStatus, string> = { PENDING_ACCEPTANCE: '待接单', WAITING_INBOUND: '待入仓', RECEIVING: '收货中', WAITING_CONSOLIDATION: '待合托', PENDING_DISPATCH: '待派车', DISPATCHED: '已派车', IN_TRANSIT: '运输中', COMPLETED: '已完成', CANCELLED: '已取消', ...poolOrderStatusLabels }
|
|
const transportTypeLabels: Record<LocalTransportType, string> = { PORT_PICKUP: '港口提柜', WAREHOUSE_TO_WAREHOUSE: '仓到仓', WAREHOUSE_TO_DOOR: '仓到门' }
|
|
const statusLabels = computed<Record<string, string>>(() => isLooseboard.value ? looseboardStatusLabels : truckStatusLabels)
|
|
const statusTones: Record<LocalTransportStatus | LocalLooseboardStatus, 'warning' | 'primary' | 'success' | 'danger' | 'info'> = {
|
|
PENDING_REVIEW: 'warning', PENDING_ACCEPTANCE: 'warning', WAITING_INBOUND: 'warning',
|
|
RECEIVING: 'primary', WAITING_CONSOLIDATION: 'warning', PALLET_OVERAGE_REVIEW: 'warning',
|
|
PARTIALLY_ALLOCATED: 'primary', ALLOCATED: 'primary', PENDING_DISPATCH: 'warning',
|
|
DISPATCHED: 'primary', IN_TRANSIT: 'primary', COMPLETED: 'success', REJECTED: 'danger', CANCELLED: 'info',
|
|
}
|
|
const fields = computed<QueryField[]>(() => [
|
|
{ key: 'orderNo', type: 'input', label: '商业单号', placeholder: '请输入商业单号', width: 230 },
|
|
{ key: 'status', type: 'select', label: '状态', placeholder: '全部状态', width: 170, options: Object.entries(isLooseboard.value ? looseboardStatusLabels : truckStatusLabels).map(([value, label]) => ({ value, label })) },
|
|
{ key: 'routeKeyword', type: 'input', label: '运输路线', placeholder: '起运地/目的地', width: 240 },
|
|
])
|
|
const truckColumns: DataTableColumn[] = [
|
|
{ key: 'actions', label: '操作', width: 220, fixed: 'left', slotName: 'actions' }, { key: 'orderNo', prop: 'orderNo', label: '订单编号', minWidth: 170 },
|
|
{ key: 'vehicleType', label: '车辆类型', minWidth: 110, formatter: (row: LocalTransportOrder) => row.vehicleType ? ({ PALLET_15: '15拖', PALLET_30: '30拖' })[row.vehicleType] : '—' },
|
|
{ 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' }, { 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}` },
|
|
{ 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, slotName: 'status' },
|
|
{ key: 'createdAt', prop: 'createdAt', label: '创建时间', minWidth: 165, formatter: (row: LocalTransportOrder) => formatDateTime(row.createdAt) },
|
|
]
|
|
const looseboardColumns: DataTableColumn[] = [
|
|
{ key: 'actions', label: '操作', width: 180, fixed: 'left', slotName: 'actions' }, { key: 'orderNo', prop: 'orderNo', label: '订单编号', minWidth: 170 },
|
|
{ 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' },
|
|
{ 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}` },
|
|
{ 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, slotName: 'status' },
|
|
{ key: 'createdAt', prop: 'createdAt', label: '创建时间', minWidth: 165, formatter: (row: LocalLooseboardOrder) => formatDateTime(row.createdAt) },
|
|
]
|
|
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
|
|
})
|
|
function formatDateTime(value?: string | null) { return value ? value.replace('T', ' ').slice(0, 16) : '—' }
|
|
async function load() {
|
|
const sourceBooking = route.query.fleetBookingNo
|
|
const sourceOrder = route.query.orderNo
|
|
arrangementOpen.value = false; arrangement.value = null
|
|
rows.value = []; total.value = 0; loadError.value = null; loading.value = true
|
|
try {
|
|
if (isLooseboard.value) {
|
|
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)
|
|
rows.value = result.records; total.value = result.total
|
|
} else {
|
|
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)
|
|
rows.value = result.records; total.value = result.total
|
|
}
|
|
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)
|
|
}
|
|
} catch (error) { loadError.value = getApiErrorDetails(error, `${domainLabel.value}订单加载失败,请稍后重试`) } finally { loading.value = false }
|
|
}
|
|
function search(model: QueryModel) { applied.value = { ...model }; if (page.value === 1) void load(); else page.value = 1 }
|
|
function openCreate() { void router.push({ name: isLooseboard.value ? 'overseas-business-looseboard-new' : 'overseas-business-truck-new' }) }
|
|
function canEdit(row: LocalTransportOrder | LocalLooseboardOrder) { return !row.fleetArrangement && (isLooseboard.value ? row.status === 'PENDING_ACCEPTANCE' : row.status === 'PENDING_REVIEW') }
|
|
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 } }) }
|
|
/** 单据详情保留订单与客户范围,费用在原业务单中处理。 */
|
|
function openDetail(row: LocalTransportOrder | LocalLooseboardOrder) {
|
|
void router.push({ name: isLooseboard.value ? 'overseas-business-looseboard-detail' : 'overseas-business-truck-detail', params: { orderId: row.orderNo }, query: { customerId: row.customerId } })
|
|
}
|
|
/** 散板详情不依赖编辑资格,已安排承运的订单也能处理自己的费用。 */
|
|
function looseboardActions(row: LocalLooseboardOrder): RowActionItem[] {
|
|
return [
|
|
{ key: 'detail', label: '详情', priority: 10 },
|
|
{ key: 'edit', label: '编辑', visible: !row.fleetArrangement && access.can(editPermission.value), disabled: !canEdit(row) },
|
|
{ key: 'arrangement', label: '承运安排', visible: !!row.fleetArrangement },
|
|
]
|
|
}
|
|
function truckActions(row: LocalTransportOrder): RowActionItem[] {
|
|
return [
|
|
{ key: 'detail', label: row.status === 'PENDING_REVIEW' && !row.fleetArrangement && access.can(overseasBusinessPermissions.truckStatus) ? '审核' : '详情', priority: 10 },
|
|
{ key: 'edit', label: '编辑', visible: !row.fleetArrangement && access.can(editPermission.value), disabled: !canEdit(row) },
|
|
{ key: 'arrangement', label: '承运安排', visible: !!row.fleetArrangement },
|
|
]
|
|
}
|
|
function handleTruckAction(key: string, row: LocalTransportOrder | LocalLooseboardOrder) {
|
|
if (key === 'edit') openEdit(row)
|
|
else if (key === 'arrangement') showArrangement(row)
|
|
else if (key === 'detail') openDetail(row)
|
|
}
|
|
let activatedOnce = false
|
|
onActivated(() => { if (activatedOnce) void load(); else activatedOnce = true })
|
|
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() })
|
|
</script>
|
|
|
|
<template>
|
|
<AdminPage class="admin-density admin-workbench"><ListPageLayout :aria-label="`${domainLabel}订单列表`">
|
|
|
|
|
|
<section><div class="workbench-table is-page-surface"><DataTable appearance="integrated" 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}订单`">
|
|
<template #toolbar-start><el-button v-if="inquiryReturnTarget(route.query)" link @click="router.push(inquiryReturnTarget(route.query)!)">返回原拼车登记</el-button><h1>{{ domainLabel }}</h1></template>
|
|
<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>
|
|
|
|
|
|
<template #toolbar-actions><el-button v-if="access.can(createPermission)" type="primary" @click="openCreate"><Plus :size="16" />新增订单</el-button></template>
|
|
<template #actions="{ row }"><RowActions :actions="isLooseboard ? looseboardActions(row as LocalLooseboardOrder) : truckActions(row as LocalTransportOrder)" text-actions @action="handleTruckAction($event, row as LocalTransportOrder | LocalLooseboardOrder)" /></template>
|
|
<template #status="{ row }"><el-tag :type="statusTones[row.status as LocalTransportStatus | LocalLooseboardStatus] || 'info'" size="small" effect="light">{{ statusLabels[row.status] || '—' }}</el-tag></template>
|
|
<template #pagination-left>共 {{ total }} 条{{ domainLabel }}订单</template>
|
|
<template #routeMap="{ row }"><el-button v-if="row.orderNo" link type="primary" @click="openRoute(row as LocalLooseboardOrder)">查看路线</el-button></template>
|
|
</DataTable></div></section>
|
|
</ListPageLayout><AppDialog v-model="arrangementOpen" class="admin-density-dialog admin-workbench is-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>
|
|
</template>
|
|
|
|
<style scoped>.table-panel { overflow:hidden; }</style>
|