优化
This commit is contained in:
@@ -3,7 +3,7 @@ import type { OceanBookingDetail, OceanBookingPage, OceanBookingQuery, OceanBook
|
||||
|
||||
const basePath = '/platform/v1/ocean-bookings'
|
||||
function pageNumber(value: number | string, fallback: number) { const n = Number(value); return Number.isFinite(n) ? Math.trunc(n) : fallback }
|
||||
function booking(raw: OceanBookingResource): OceanBookingResource { return { ...raw, id: String(raw.id), customerId: raw.customerId == null ? null : String(raw.customerId), containers: (raw.containers || []).map((item) => ({ ...item, id: String(item.id) })) } }
|
||||
function booking(raw: OceanBookingResource): OceanBookingResource { const item = raw as OceanBookingResource & { departurePortName?: string | null; destinationPortName?: string | null; shippingCompanyName?: string | null; estimatedSailingAt?: string | null; estimatedArrivalAt?: string | null }; return { ...raw, id: String(raw.id), customerId: raw.customerId == null ? null : String(raw.customerId), pol: raw.pol ?? item.departurePortName ?? null, pod: raw.pod ?? item.destinationPortName ?? null, carrierName: raw.carrierName ?? item.shippingCompanyName ?? null, etd: raw.etd ?? item.estimatedSailingAt ?? null, eta: raw.eta ?? item.estimatedArrivalAt ?? null, containers: (raw.containers || []).map((container) => ({ ...container, id: String(container.id ?? '') })) } }
|
||||
export async function listOceanBookings(query: OceanBookingQuery) {
|
||||
const response = await http.get<ApiEnvelope<OceanBookingPage>, ApiEnvelope<OceanBookingPage>>(basePath, { params: query })
|
||||
const result = unwrapApiResponse(response)
|
||||
|
||||
@@ -13,7 +13,7 @@ const router = useRouter(); const rows = ref<OceanBookingResource[]>([]); const
|
||||
const query = reactive<QueryModel>({ keyword: '', status: '', pol: '', pod: '', soNo: '' })
|
||||
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' }]
|
||||
const fields: QueryField[] = [{ key: 'keyword', type: 'input', label: '关键词', placeholder: '订舱号/订单号/客户', width: 220 }, { key: 'status', type: 'select', label: '状态', options: statuses }, { key: 'pol', type: 'input', label: '起运港', width: 150 }, { key: 'pod', type: 'input', label: '目的港', width: 150 }, { key: 'soNo', type: 'input', label: 'S/O号', width: 150 }]
|
||||
const columns = [{ key: 'actions', label: '操作', fixed: 'left', width: 170, slotName: 'actions', hideable: false, movable: false }, { prop: 'bookingNo', label: '订舱编号', minWidth: 150 }, { prop: 'orderNo', label: '关联订单', minWidth: 140 }, { prop: 'customerName', label: '客户', minWidth: 140 }, { prop: 'pol', label: '起运港', minWidth: 110 }, { prop: 'pod', label: '目的港', minWidth: 110 }, { prop: 'vesselName', label: '船名/航次', minWidth: 150, slotName: 'vessel' }, { prop: 'soNo', label: 'S/O号', minWidth: 130 }, { prop: 'cutoffAt', label: '截关时间', minWidth: 150 }, { prop: 'status', label: '状态', minWidth: 110, slotName: 'status' }]
|
||||
const columns = [{ key: 'actions', label: '操作', fixed: 'left', width: 170, slotName: 'actions', hideable: false, movable: false }, { prop: 'bookingNo', label: '订舱编号', minWidth: 150 }, { prop: 'orderNo', label: '关联订单', minWidth: 140 }, { prop: 'customerName', label: '委托单位', minWidth: 160 }, { prop: 'mblShipper', label: 'MB/L货人', minWidth: 150 }, { prop: 'mblConsignee', label: 'MB/L收人', minWidth: 150 }, { prop: 'pol', label: '起运港(POL)', minWidth: 125 }, { prop: 'pod', label: '目的港(POD)', minWidth: 125 }, { prop: 'containerSummary', label: '箱型/箱量', minWidth: 120 }, { prop: 'carrierName', label: '船公司', minWidth: 130 }, { prop: 'vesselName', label: '船名/航次', minWidth: 150, slotName: 'vessel' }, { prop: 'soNo', label: 'S/O号', minWidth: 130 }, { prop: 'cutoffAt', label: '预计截关', minWidth: 150 }, { prop: 'etd', label: 'ETD', minWidth: 140 }, { prop: 'atd', label: 'ATD', minWidth: 140 }, { prop: 'eta', label: 'ETA', minWidth: 140 }, { prop: 'ata', label: 'ATA', minWidth: 140 }, { prop: 'status', label: '状态', minWidth: 110, slotName: 'status' }]
|
||||
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, pol: String(query.pol || '') || undefined, pod: String(query.pod || '') || undefined, soNo: String(query.soNo || '') || undefined } satisfies OceanBookingQuery); rows.value = result.records; total.value = result.total } catch (e) { error.value = getApiErrorDetails(e) } finally { loading.value = false } }
|
||||
function search() { page.value = 1; void load() }; function open(id?: string) { void router.push(id ? `/ocean-bookings/${id}` : '/ocean-bookings/new') }
|
||||
watch([page, pageSize], () => void load()); onMounted(load)
|
||||
|
||||
@@ -8,8 +8,7 @@ import type {
|
||||
|
||||
const basePath = '/platform/v1/truck-appointments'
|
||||
function dtoPayload(payload: SaveTruckAppointmentRequest) {
|
||||
const { attachmentRefs: _attachmentRefs, ...dto } = payload
|
||||
return dto
|
||||
return Object.fromEntries(Object.entries(payload).map(([key, value]) => [key, value === '' ? null : value])) as SaveTruckAppointmentRequest
|
||||
}
|
||||
|
||||
type PagePayload = Omit<TruckAppointmentPage, 'page' | 'size' | 'total' | 'pages'> & {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { truckAppointmentStatusLabels, type SaveTruckAppointmentRequest, type TruckAppointmentResource } from '../types'
|
||||
import type { SaveTruckAppointmentRequest, TruckAppointmentResource } from '../types'
|
||||
|
||||
const props = defineProps<{ modelValue: SaveTruckAppointmentRequest; resource?: TruckAppointmentResource | null; readonly?: boolean; submitting?: boolean }>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [SaveTruckAppointmentRequest]; submit: []; attachments: [] }>()
|
||||
@@ -14,6 +14,7 @@ const rules: FormRules = {
|
||||
}
|
||||
const relationText = computed(() => [props.resource?.customerName, props.resource?.outboundOrderId, props.resource?.containerBookingId].filter(Boolean).join(' / ') || '由服务端关联资源返回')
|
||||
async function validate() { return formRef.value?.validate() }
|
||||
async function submitForm() { if (await formRef.value?.validate()) emit('submit') }
|
||||
defineExpose({ validate })
|
||||
</script>
|
||||
|
||||
@@ -21,11 +22,19 @@ defineExpose({ validate })
|
||||
<el-form ref="formRef" :model="draft" :rules="rules" label-position="top" :disabled="readonly || submitting" class="truck-form">
|
||||
<el-alert v-if="resource" type="info" :closable="false" class="relation-alert">关联资源:{{ relationText }};柜型由关联柜体返回(只读)</el-alert>
|
||||
<el-row :gutter="16">
|
||||
<el-col :xs="24" :sm="12"><el-form-item label="柜体关联 ID"><el-input v-model="draft.containerBookingId" placeholder="请输入柜体预约 ID" /></el-form-item></el-col>
|
||||
<el-col :xs="24" :sm="12"><el-form-item label="出仓单关联 ID"><el-input v-model="draft.outboundOrderId" placeholder="请输入出仓单 ID" /></el-form-item></el-col>
|
||||
<el-col :xs="24" :sm="12"><el-form-item label="海运订舱关联 ID"><el-input v-model="draft.oceanBookingId" /></el-form-item></el-col>
|
||||
<el-col :xs="24" :sm="12"><el-form-item label="装柜计划关联 ID"><el-input v-model="draft.loadingPlanId" /></el-form-item></el-col>
|
||||
<el-col :xs="24" :sm="12"><el-form-item label="仓库关联 ID"><el-input v-model="draft.warehouseId" /></el-form-item></el-col>
|
||||
<el-col :xs="24" :sm="12"><el-form-item label="客户关联 ID"><el-input v-model="draft.customerId" /></el-form-item></el-col>
|
||||
<el-col :xs="24" :sm="12"><el-form-item label="承运商 ID" prop="carrierId"><el-input v-model="draft.carrierId" placeholder="请输入后端资源 ID" /></el-form-item></el-col>
|
||||
<el-col :xs="24" :sm="12"><el-form-item label="承运商名称"><el-input v-model="draft.carrierName" /></el-form-item></el-col>
|
||||
<el-col :xs="24" :sm="12"><el-form-item label="车队联系人"><el-input v-model="draft.fleetContact" /></el-form-item></el-col>
|
||||
<el-col :xs="24" :sm="12"><el-form-item label="车队电话"><el-input v-model="draft.fleetPhone" /></el-form-item></el-col>
|
||||
<el-col :xs="24" :sm="12"><el-form-item label="司机"><el-input v-model="draft.driverName" /></el-form-item></el-col>
|
||||
<el-col :xs="24" :sm="12"><el-form-item label="司机电话"><el-input v-model="draft.driverPhone" /></el-form-item></el-col>
|
||||
<el-col :xs="24" :sm="12"><el-form-item label="司机身份证号"><el-input v-model="draft.driverIdCard" /></el-form-item></el-col>
|
||||
<el-col :xs="24" :sm="12"><el-form-item label="车头号"><el-input v-model="draft.vehicleNo" /></el-form-item></el-col>
|
||||
<el-col :xs="24" :sm="12"><el-form-item label="预计装箱时间" prop="plannedLoadingAt"><el-date-picker v-model="draft.plannedLoadingAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col v-if="resource" :xs="24" :sm="12"><el-form-item label="实际装箱时间"><el-input :model-value="resource.actualLoadingAt || ''" disabled /></el-form-item></el-col>
|
||||
@@ -33,16 +42,20 @@ defineExpose({ validate })
|
||||
<el-col :xs="24" :sm="8"><el-form-item label="毛重(kg)"><el-input v-model="draft.grossWeightKg" /></el-form-item></el-col>
|
||||
<el-col :xs="24" :sm="8"><el-form-item label="体积(m³)"><el-input v-model="draft.volumeCbm" /></el-form-item></el-col>
|
||||
<el-col :xs="24" :sm="8"><el-form-item label="托盘数量"><el-input v-model="draft.palletCount" inputmode="numeric" /></el-form-item></el-col>
|
||||
<el-col :xs="24" :sm="8"><el-form-item label="在途分钟"><el-input v-model="draft.transitMinutes" inputmode="numeric" /></el-form-item></el-col>
|
||||
<el-col :xs="24" :sm="8"><el-form-item label="应付拖车费"><el-input v-model="draft.payableAmount"><template #append>RMB</template></el-input></el-form-item></el-col>
|
||||
<el-col :xs="24" :sm="8"><el-form-item label="应收拖车费"><el-input v-model="draft.receivableAmount"><template #append>RMB</template></el-input></el-form-item></el-col>
|
||||
<el-col :xs="24" :sm="8"><el-form-item label="币种"><el-input v-model="draft.currency" maxlength="8" /></el-form-item></el-col>
|
||||
<el-col :xs="24" :sm="12"><el-form-item label="门店名称"><el-input v-model="draft.storeName" /></el-form-item></el-col>
|
||||
<el-col :xs="24" :sm="12"><el-form-item label="门店地址"><el-input v-model="draft.storeAddress" /></el-form-item></el-col>
|
||||
<el-col :xs="24" :sm="12"><el-form-item label="门店联系人"><el-input v-model="draft.storeContact" /></el-form-item></el-col>
|
||||
<el-col :xs="24" :sm="12"><el-form-item label="门店电话"><el-input v-model="draft.storePhone" /></el-form-item></el-col>
|
||||
<el-col :xs="24" :sm="12"><el-form-item label="城市"><el-input v-model="draft.storeCity" /></el-form-item></el-col>
|
||||
<el-col :xs="24" :sm="12"><el-form-item label="邮编"><el-input v-model="draft.storeZipcode" /></el-form-item></el-col>
|
||||
<el-col :xs="24"><el-form-item label="PO"><el-input v-model="draft.poNo" /></el-form-item></el-col>
|
||||
<el-col :xs="24"><el-form-item label="备注"><el-input v-model="draft.remark" type="textarea" :rows="3" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<div class="form-actions"><el-button :disabled="readonly || submitting" @click="emit('attachments')">管理附件引用</el-button><el-button v-if="!readonly" type="primary" :loading="submitting" @click="emit('submit')">保存</el-button></div>
|
||||
<div class="form-actions"><el-button :disabled="submitting" @click="emit('attachments')">查看附件引用</el-button><el-button v-if="!readonly" type="primary" :loading="submitting" @click="submitForm">保存</el-button></div>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,27 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, watch } from 'vue'
|
||||
import type { TruckAttachmentReference } from '../types'
|
||||
const props = defineProps<{ modelValue: boolean; attachments: TruckAttachmentReference[]; readonly?: boolean }>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [boolean]; save: [TruckAttachmentReference[]] }>()
|
||||
const draft = reactive<TruckAttachmentReference[]>([])
|
||||
watch(() => props.attachments, (value) => { draft.splice(0, draft.length, ...(value || []).map((item) => ({ ...item }))) }, { immediate: true, deep: true })
|
||||
function add() { draft.push({ fileId: '', fileName: '' }) }
|
||||
function remove(index: number) { draft.splice(index, 1) }
|
||||
function save() { emit('save', draft.filter((item) => item.fileId.trim() && item.fileName.trim()).map((item) => ({ ...item }))); emit('update:modelValue', false) }
|
||||
const props = defineProps<{ modelValue: boolean; attachments: TruckAttachmentReference[] }>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [boolean] }>()
|
||||
</script>
|
||||
<template>
|
||||
<el-dialog :model-value="props.modelValue" title="附件引用" width="min(640px, 94vw)" @update:model-value="emit('update:modelValue', $event)">
|
||||
<el-alert type="info" :closable="false" title="此处仅维护文件服务返回的引用,不会模拟上传成功。" />
|
||||
<div v-for="(item, index) in draft" :key="index" class="attachment-row">
|
||||
<el-input v-model="item.fileId" placeholder="文件 ID" :disabled="readonly" />
|
||||
<el-input v-model="item.fileName" placeholder="文件名" :disabled="readonly" />
|
||||
<el-button text type="danger" :disabled="readonly" @click="remove(index)">删除</el-button>
|
||||
<el-alert type="info" :closable="false" title="仅展示后端返回的真实文件引用;当前接口未提供上传或删除动作。" />
|
||||
<el-empty v-if="attachments.length === 0" description="暂无真实附件引用" :image-size="72" />
|
||||
<div v-for="item in attachments" :key="item.fileId" class="attachment-row">
|
||||
<div><strong>{{ item.fileName }}</strong><small>文件 ID:{{ item.fileId }}</small></div>
|
||||
<code v-if="item.objectKey">{{ item.objectKey }}</code>
|
||||
</div>
|
||||
<el-button v-if="!readonly" plain @click="add">新增引用</el-button>
|
||||
<template #footer><el-button @click="emit('update:modelValue', false)">关闭</el-button><el-button v-if="!readonly" type="primary" @click="save">保存引用</el-button></template>
|
||||
<template #footer><el-button type="primary" @click="emit('update:modelValue', false)">关闭</el-button></template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<style scoped>
|
||||
.attachment-row { display: grid; grid-template-columns: 1fr 1fr auto; gap: 8px; margin: 14px 0; }
|
||||
@media (max-width: 560px) { .attachment-row { grid-template-columns: 1fr; } }
|
||||
.attachment-row { display:flex; justify-content:space-between; gap:12px; margin:14px 0; padding:12px; border:1px solid var(--el-border-color-lighter); border-radius:6px; overflow-wrap:anywhere; }.attachment-row small { display:block; margin-top:4px; color:var(--el-text-color-secondary); }.attachment-row code { color:var(--el-text-color-secondary); font-size:12px; }
|
||||
@media (max-width: 560px) { .attachment-row { flex-direction:column; } }
|
||||
</style>
|
||||
|
||||
@@ -8,10 +8,9 @@ import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import TruckAppointmentForm from '../components/TruckAppointmentForm.vue'
|
||||
import TruckAttachmentDialog from '../components/TruckAttachmentDialog.vue'
|
||||
import { createTruckAppointment } from '../api/platformTrucking'
|
||||
import type { SaveTruckAppointmentRequest, TruckAttachmentReference } from '../types'
|
||||
import type { SaveTruckAppointmentRequest } from '../types'
|
||||
const router = useRouter(); const open = ref(true); const saving = ref(false); const attachmentOpen = ref(false); const error = ref<ApiErrorDetails | null>(null)
|
||||
const form = reactive<SaveTruckAppointmentRequest>({ attachmentRefs: [], expectedVersion: 0, plannedLoadingAt: null, payableAmount: '0.00', receivableAmount: '0.00', currency: 'RMB' })
|
||||
async function submit() { saving.value = true; error.value = null; try { await createTruckAppointment({ ...form, attachmentRefs: form.attachmentRefs || [] }); ElMessage.success('拖车预约已创建'); router.push({ name: 'trucking' }) } catch (err) { error.value = getApiErrorDetails(err, '创建失败,请稍后重试') } finally { saving.value = false } }
|
||||
function saveAttachments(items: TruckAttachmentReference[]) { form.attachmentRefs = items }
|
||||
const form = reactive<SaveTruckAppointmentRequest>({ expectedVersion: 0, plannedLoadingAt: null, payableAmount: '0.00', receivableAmount: '0.00', currency: 'RMB' })
|
||||
async function submit() { saving.value = true; error.value = null; try { await createTruckAppointment({ ...form }); ElMessage.success('拖车预约已创建'); router.push({ name: 'trucking' }) } catch (err) { error.value = getApiErrorDetails(err, '创建失败,请稍后重试') } finally { saving.value = false } }
|
||||
</script>
|
||||
<template><AppDrawer v-model="open" title="新增拖车预约" :confirm-loading="saving" :show-footer="false" @cancel="router.push({ name: 'trucking' })"><ApiErrorMeta v-if="error" :details="error" /><TruckAppointmentForm v-model="form" :submitting="saving" @submit="submit" @attachments="attachmentOpen = true" /><TruckAttachmentDialog v-model="attachmentOpen" :attachments="form.attachmentRefs || []" @save="saveAttachments" /></AppDrawer></template>
|
||||
<template><AppDrawer v-model="open" title="新增拖车预约" :confirm-loading="saving" :show-footer="false" @cancel="router.push({ name: 'trucking' })"><ApiErrorMeta v-if="error" :details="error" /><TruckAppointmentForm v-model="form" :submitting="saving" @submit="submit" @attachments="attachmentOpen = true" /><TruckAttachmentDialog v-model="attachmentOpen" :attachments="[]" /></AppDrawer></template>
|
||||
|
||||
@@ -10,22 +10,28 @@ import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { getTruckAppointment, performTruckAppointmentAction, rescheduleTruckAppointment, updateTruckAppointment, type TruckAppointmentAction } from '../api/platformTrucking'
|
||||
import TruckAppointmentForm from '../components/TruckAppointmentForm.vue'
|
||||
import TruckAttachmentDialog from '../components/TruckAttachmentDialog.vue'
|
||||
import { TRUCK_APPOINTMENT_STATUSES, truckAppointmentStatusLabels, type SaveTruckAppointmentRequest, type TruckAppointmentResource, type TruckAppointmentStatus, type TruckAttachmentReference } from '../types'
|
||||
import { truckAppointmentStatusLabels, type SaveTruckAppointmentRequest, type TruckAppointmentResource, type TruckAppointmentStatus } from '../types'
|
||||
const route = useRoute(); const router = useRouter(); const open = ref(true); const loading = ref(false); const saving = ref(false); const statusSaving = ref(false); const attachmentOpen = ref(false); const resource = ref<TruckAppointmentResource | null>(null); const error = ref<ApiErrorDetails | null>(null); const form = reactive<SaveTruckAppointmentRequest>({ expectedVersion: 0 })
|
||||
const access = useAccessPreviewStore(); const editing = computed(() => route.query.edit === '1'); const id = computed(() => String(route.params.id)); const canEdit = computed(() => access.can('platform:firstleg:truck-appointment:edit') && !['COMPLETED','CANCELLED'].includes(resource.value?.status || '') && editing.value); const canChangeStatus = computed(() => access.can('platform:firstleg:truck-appointment:operate'))
|
||||
async function load() { loading.value = true; error.value = null; try { const result = await getTruckAppointment(id.value); resource.value = result; Object.assign(form, { ...result, expectedVersion: result.version ?? 0, attachmentRefs: result.attachments || [] }) } catch (err) { error.value = getApiErrorDetails(err, '详情加载失败,请稍后重试') } finally { loading.value = false } }
|
||||
async function load() { loading.value = true; error.value = null; try { const result = await getTruckAppointment(id.value); resource.value = result; Object.assign(form, { ...result, expectedVersion: result.version ?? 0 }) } catch (err) { error.value = getApiErrorDetails(err, '详情加载失败,请稍后重试') } finally { loading.value = false } }
|
||||
async function submit() { saving.value = true; error.value = null; try { await updateTruckAppointment(id.value, form); ElMessage.success('拖车预约已保存'); await load() } catch (err) { error.value = getApiErrorDetails(err, '保存失败,请稍后重试') } finally { saving.value = false } }
|
||||
const actionByTarget: Partial<Record<TruckAppointmentStatus, TruckAppointmentAction>> = { PENDING_DISPATCH: 'SUBMIT', DISPATCHED: 'DISPATCH', PICKUP_IN_PROGRESS: 'PICKUP', ARRIVED_WAREHOUSE: 'ARRIVE', LOADING: 'LOAD', DEPARTED: 'DEPART', COMPLETED: 'COMPLETE', EXCEPTION: 'EXCEPTION', CANCELLED: 'CANCEL' }
|
||||
const primaryTargetByStatus: Partial<Record<TruckAppointmentStatus, TruckAppointmentStatus>> = { DRAFT: 'PENDING_DISPATCH', PENDING_DISPATCH: 'DISPATCHED', RESCHEDULED: 'DISPATCHED', DISPATCHED: 'PICKUP_IN_PROGRESS', PICKUP_IN_PROGRESS: 'ARRIVED_WAREHOUSE', ARRIVED_WAREHOUSE: 'LOADING', LOADING: 'DEPARTED', DEPARTED: 'COMPLETED' }
|
||||
const availableTargets = computed<TruckAppointmentStatus[]>(() => {
|
||||
if (!resource.value || ['COMPLETED', 'CANCELLED'].includes(resource.value.status)) return []
|
||||
const primary = primaryTargetByStatus[resource.value.status]
|
||||
return [...(primary ? [primary] : []), 'RESCHEDULED', 'EXCEPTION', 'CANCELLED']
|
||||
})
|
||||
async function changeStatus(status: TruckAppointmentStatus) { if (!resource.value || status === resource.value.status) return; try { await ElMessageBox.confirm(`确认将状态改为“${truckAppointmentStatusLabels[status]}”?`, '状态变更确认', { type: 'warning' }); statusSaving.value = true; if (status === 'RESCHEDULED') { await rescheduleTruckAppointment(id.value, form.plannedLoadingAt || '', form.expectedVersion || 0, '后台改约') } else { const action = actionByTarget[status]; if (!action) throw new Error('当前状态没有可用动作'); await performTruckAppointmentAction(id.value, action, form.expectedVersion || 0) } ElMessage.success('状态已更新'); await load() } catch (err) { if (err !== 'cancel' && !(err instanceof Error && err.message === 'cancel')) error.value = getApiErrorDetails(err, '状态更新失败,请稍后重试') } finally { statusSaving.value = false } }
|
||||
function saveAttachments(items: TruckAttachmentReference[]) { form.attachmentRefs = items }
|
||||
onMounted(load)
|
||||
</script>
|
||||
<template>
|
||||
<AppDrawer v-model="open" :title="resource?.appointmentNo ? `拖车预约 ${resource.appointmentNo}` : '拖车预约详情'" :loading="loading" :show-footer="false" @cancel="router.push({ name: 'trucking' })">
|
||||
<LoadErrorState v-if="error && !resource" title="详情加载失败" :message="error.message" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
|
||||
<template v-else-if="resource"><div class="status-bar"><span>当前状态:<strong>{{ truckAppointmentStatusLabels[resource.status] }}</strong></span><el-select :model-value="resource.status" :loading="statusSaving" :disabled="statusSaving || !canChangeStatus" style="width:170px" @change="changeStatus"><el-option v-for="status in TRUCK_APPOINTMENT_STATUSES" :key="status" :value="status" :label="truckAppointmentStatusLabels[status]" /></el-select></div><ApiErrorMeta v-if="error" :details="error" /><TruckAppointmentForm v-model="form" :resource="resource" :readonly="!canEdit" :submitting="saving" @submit="submit" @attachments="attachmentOpen = true" /><TruckAttachmentDialog v-model="attachmentOpen" :attachments="form.attachmentRefs || []" :readonly="!canEdit" @save="saveAttachments" /></template>
|
||||
<template v-else-if="resource"><div class="status-bar"><span>当前状态:<strong>{{ truckAppointmentStatusLabels[resource.status] }}</strong></span><div v-if="canChangeStatus" class="status-actions"><el-button v-for="status in availableTargets" :key="status" :type="status === 'CANCELLED' ? 'danger' : status === 'EXCEPTION' ? 'warning' : 'primary'" plain :loading="statusSaving" @click="changeStatus(status)">{{ truckAppointmentStatusLabels[status] }}</el-button></div></div><ApiErrorMeta v-if="error" :details="error" /><TruckAppointmentForm v-model="form" :resource="resource" :readonly="!canEdit" :submitting="saving" @submit="submit" @attachments="attachmentOpen = true" /><TruckAttachmentDialog v-model="attachmentOpen" :attachments="resource.attachments || []" /></template>
|
||||
</AppDrawer>
|
||||
</template>
|
||||
<style scoped>
|
||||
.status-bar { display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; margin-bottom:16px; padding:12px; background:var(--el-fill-color-light); border-radius:6px; }
|
||||
.status-actions { display:flex; flex-wrap:wrap; gap:8px; }.status-actions :deep(.el-button) { margin-left:0; }
|
||||
</style>
|
||||
|
||||
@@ -27,12 +27,13 @@ const columns: DataTableColumn[] = [
|
||||
{ key: 'company', label: '承运商', minWidth: 150, slotName: 'company' },
|
||||
{ key: 'driver', label: '司机/车头', minWidth: 150, slotName: 'driver' },
|
||||
{ key: 'store', label: '门店', minWidth: 130, slotName: 'store' },
|
||||
{ key: 'plannedLoadingAt', prop: 'plannedLoadingAt', label: '预计装箱时间', minWidth: 170 },
|
||||
{ key: 'plannedLoadingAt', label: '预计装箱时间', minWidth: 170, formatter: (row: TruckAppointmentResource) => formatDateTime(row.plannedLoadingAt) },
|
||||
{ key: 'payableAmount', label: '应付拖车费', minWidth: 130, formatter: (row: TruckAppointmentResource) => row.payableAmount ? `¥${row.payableAmount}` : '—' },
|
||||
{ key: 'receivableAmount', label: '应收拖车费', minWidth: 130, formatter: (row: TruckAppointmentResource) => row.receivableAmount ? `¥${row.receivableAmount}` : '—' },
|
||||
{ key: 'status', label: '状态', width: 130, slotName: 'status' },
|
||||
{ key: 'attachments', label: '附件', width: 90, formatter: (row: TruckAppointmentResource) => String(row.attachments?.length || 0) },
|
||||
]
|
||||
function formatDateTime(value?: string | null) { return value ? value.replace('T', ' ').slice(0, 16) : '—' }
|
||||
function statusLabel(value: unknown) {
|
||||
return typeof value === 'string' && value in truckAppointmentStatusLabels
|
||||
? truckAppointmentStatusLabels[value as TruckAppointmentStatus]
|
||||
@@ -44,25 +45,29 @@ function reset() { filters.value = { keyword: '', status: '', containerBookingId
|
||||
onMounted(load)
|
||||
</script>
|
||||
<template>
|
||||
<section class="page-shell">
|
||||
<div class="page-heading"><div><h1>拖车预约</h1><p>维护拖车预约、关联资源与派车状态</p></div><div class="heading-actions"><el-button v-if="access.can('platform:firstleg:truck-appointment:create')" type="primary" @click="router.push({ name: 'trucking-new' })"><Plus :size="16" />新增预约</el-button></div></div>
|
||||
<LoadErrorState v-if="loadError" title="拖车预约加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState>
|
||||
<QueryToolbar v-model="filters" ariaLabel="拖车预约查询" :fields="fields" @search="search" @reset="reset" />
|
||||
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" preference-key="trucking-appointments">
|
||||
<template #relation="{ row }"><div>{{ row.customerName || '—' }}</div><small>出仓单 {{ row.outboundOrderId || '—' }} · 柜体 {{ row.containerBookingId || '—' }}</small></template>
|
||||
<section class="business-page trucking-page">
|
||||
<div class="page-heading"><div><h1>拖车预约</h1><p>维护拖车预约、关联资源与派车状态</p></div></div>
|
||||
<QueryToolbar v-model="filters" class="surface-panel" ariaLabel="拖车预约查询" :fields="fields" preference-key="trucking-query" @search="search" @reset="reset" />
|
||||
<LoadErrorState v-if="loadError" class="surface-panel" title="拖车预约加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState>
|
||||
<section class="surface-panel table-panel" :aria-busy="loading">
|
||||
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" preference-key="trucking-appointments" aria-label="拖车预约列表" empty-text="暂无符合筛选条件的拖车预约">
|
||||
<template #toolbar-start><div class="table-heading"><h2>拖车预约列表</h2><span>共 {{ total }} 条</span></div></template>
|
||||
<template #toolbar-actions><el-button v-if="access.can('platform:firstleg:truck-appointment:create')" type="primary" :disabled="loading" @click="router.push({ name: 'trucking-new' })"><Plus :size="16" />新增拖车</el-button></template>
|
||||
<template #relation="{ row }"><div>{{ row.customerName || '—' }}</div><small>{{ row.entrustingPartyName || '委托单位待关联' }} · 出仓单 {{ row.outboundOrderId || '—' }} · 柜体 {{ row.containerBookingId || '—' }}</small><small>柜型 {{ row.relatedContainerType || '由关联资源返回' }}</small></template>
|
||||
<template #company="{ row }">{{ row.carrierName || row.carrierId || '—' }}</template>
|
||||
<template #driver="{ row }">{{ row.driverName || '—' }}<small>{{ row.vehicleNo || '—' }}</small></template>
|
||||
<template #store="{ row }">{{ row.storeName || '—' }}</template>
|
||||
<template #status="{ row }"><StatusBadge :status="statusLabel(row.status)" /></template>
|
||||
<template #actions="{ row }"><el-button text :aria-label="`查看 ${row.appointmentNo || row.id}`" @click="router.push({ name: 'trucking-detail', params: { id: String(row.id) } })"><Eye :size="16" />查看</el-button><el-button v-if="access.can('platform:firstleg:truck-appointment:edit')" text :aria-label="`编辑 ${row.appointmentNo || row.id}`" @click="router.push({ name: 'trucking-detail', params: { id: String(row.id) }, query: { edit: '1' } })"><Pencil :size="16" />编辑</el-button></template>
|
||||
</DataTable>
|
||||
</section>
|
||||
<div v-if="!loading && loaded && rows.length === 0 && !loadError" class="empty-hint"><RefreshCw :size="16" />暂无拖车预约</div>
|
||||
</section>
|
||||
</template>
|
||||
<style scoped>
|
||||
.page-shell { min-width: 0; padding: 24px; }
|
||||
.trucking-page { min-width: 0; padding: 24px; }
|
||||
.page-heading { display:flex; justify-content:space-between; gap:16px; align-items:flex-start; margin-bottom:18px; flex-wrap:wrap; }
|
||||
h1 { margin:0; font-size:24px; }.page-heading p { margin:6px 0 0; color:var(--el-text-color-secondary); }.heading-actions { display:flex; gap:8px; flex-wrap:wrap; }.heading-actions :deep(.el-button) { margin:0; }.heading-actions :deep(svg) { vertical-align:middle; margin-right:5px; }.empty-hint { display:flex; justify-content:center; gap:8px; color:var(--el-text-color-secondary); padding:24px; }
|
||||
h1 { margin:0; font-size:24px; }.page-heading p { margin:6px 0 0; color:var(--el-text-color-secondary); }.surface-panel { margin-bottom:16px; }.table-panel { overflow:hidden; }.table-heading { display:flex; align-items:baseline; gap:10px; }.table-heading h2 { margin:0; font-size:16px; }.table-heading span { color:var(--el-text-color-secondary); font-size:13px; }.empty-hint { display:flex; justify-content:center; gap:8px; color:var(--el-text-color-secondary); padding:24px; }
|
||||
small { display:block; color:var(--el-text-color-secondary); margin-top:3px; }
|
||||
@media (max-width:600px) { .page-shell { padding:16px 12px; } h1 { font-size:20px; } }
|
||||
@media (max-width:600px) { .trucking-page { padding:16px 12px; } h1 { font-size:20px; } }
|
||||
</style>
|
||||
|
||||
@@ -76,6 +76,8 @@ export interface TruckAppointmentResource {
|
||||
orderId?: string | null
|
||||
orderNo?: string | null
|
||||
containerNo?: string | null
|
||||
relatedContainerType?: string | null
|
||||
entrustingPartyName?: string | null
|
||||
attachments?: TruckAttachmentReference[] | null
|
||||
createdAt?: string | null
|
||||
updatedAt?: string | null
|
||||
@@ -114,7 +116,6 @@ export interface SaveTruckAppointmentRequest {
|
||||
currency?: string | null
|
||||
poNo?: string | null
|
||||
remark?: string | null
|
||||
attachmentRefs?: TruckAttachmentReference[]
|
||||
expectedVersion: number
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user