feat(looseboard): align platform route contacts and required times

This commit is contained in:
2026-10-19 15:11:15 +08:00
parent f7f6637c45
commit 767c6b7589
7 changed files with 129 additions and 69 deletions
@@ -14,13 +14,12 @@ import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import { CONTAINER_SPECIFICATIONS, DEFAULT_CONTAINER_TYPE } from '@/shared/domain/containerSpecifications'
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
import { createLocalLooseboardOrder, createUsTruckOrder, getLocalLooseboardOrder, getUsTruckOrder, updateLocalLooseboardOrder, updateUsTruckOrder } from '../api/platformOverseasBusiness'
import { overseasBusinessPermissions } from '../permissions'
import FleetArrangementSummary from '../components/FleetArrangementSummary.vue'
import type { FleetArrangement } from '../fleetArrangement'
import type { CreateLocalLooseboardOrderRequest, CreateLocalTransportOrderRequest, LocalLooseboardStatus, LocalTransportStatus, LocalTransportType, LooseboardTransportMode, PalletRequired, UpdateLocalLooseboardOrderRequest, UpdateLocalTransportOrderRequest, TruckVehicleType } from '../types'
import type { CreateLocalLooseboardOrderRequest, CreateLocalTransportOrderRequest, LocalLooseboardStatus, LocalTransportStatus, UpdateLocalLooseboardOrderRequest, UpdateLocalTransportOrderRequest, TruckVehicleType } from '../types'
const props = defineProps<{ domain: 'truck' | 'looseboard' }>()
const routeOpen = ref(false)
@@ -32,40 +31,37 @@ const domainLabel = computed(() => isLooseboard.value ? '本土散板' : '本土
const requiredPermission = computed(() => isLooseboard.value ? (editing.value ? overseasBusinessPermissions.looseboardEdit : overseasBusinessPermissions.looseboardCreate) : (editing.value ? overseasBusinessPermissions.truckEdit : overseasBusinessPermissions.truckCreate))
type FormStatus = LocalTransportStatus | LocalLooseboardStatus
interface TransportFormModel {
customerId: string; orderNo: string; transportMode: LooseboardTransportMode | ''; palletRequired: PalletRequired | ''
transportType: LocalTransportType | ''; origin: string; destination: string; originAddress: string | null; destinationAddress: string | null
contactName: string | null; contactInfo: string | null; containerType: string | null; packageCount: number
customerId: string; orderNo: string; productName: string; estimatedPalletCount: number
origin: string; destination: string; originAddress: string | null; destinationAddress: string | null
packageCount: number
vehicleType: TruckVehicleType | ''; originContactName: string; originContactPhone: string
destinationContactName: string; destinationContactPhone: string; pickupAt: string; deliveryAt: string
totalWeightKg: string | null; totalVolumeCbm: string | null; budgetAmount: string | null; currency: string
requestedDate: string | null; remark: string | null; status: FormStatus; version: number; idempotencyKey: string
remark: string | null; status: FormStatus; version: number; idempotencyKey: string
}
function newIdempotencyKey() { return globalThis.crypto?.randomUUID?.() || `admin-${Date.now()}-${Math.random().toString(36).slice(2)}` }
const initial = (): TransportFormModel => ({ customerId: '', orderNo: '', transportMode: '', palletRequired: '', transportType: '', origin: '', destination: '', originAddress: null, destinationAddress: null, contactName: null, contactInfo: null, vehicleType: '', originContactName: '', originContactPhone: '', destinationContactName: '', destinationContactPhone: '', pickupAt: '', deliveryAt: '', containerType: DEFAULT_CONTAINER_TYPE, packageCount: 0, totalWeightKg: null, totalVolumeCbm: null, budgetAmount: null, currency: 'USD', requestedDate: null, remark: null, status: isLooseboard.value ? 'PENDING_ACCEPTANCE' : 'PENDING_REVIEW', version: 0, idempotencyKey: newIdempotencyKey() })
const initial = (): TransportFormModel => ({ customerId: '', orderNo: '', productName: '', estimatedPalletCount: 0, origin: '', destination: '', originAddress: null, destinationAddress: null, vehicleType: '', originContactName: '', originContactPhone: '', destinationContactName: '', destinationContactPhone: '', pickupAt: '', deliveryAt: '', packageCount: 0, totalWeightKg: isLooseboard.value ? '0' : null, totalVolumeCbm: isLooseboard.value ? '0' : null, budgetAmount: null, currency: 'USD', remark: null, status: isLooseboard.value ? 'PENDING_ACCEPTANCE' : 'PENDING_REVIEW', version: 0, idempotencyKey: newIdempotencyKey() })
const form = reactive<TransportFormModel>(initial()); const loading = ref(false); const saving = ref(false)
const fleetArrangement = ref<FleetArrangement | null>(null)
const loadError = ref<ApiErrorDetails | null>(null); const submitError = ref<ApiErrorDetails | null>(null)
const customers = ref<PlatformTenantResource[]>([]); const customersLoading = ref(false); const customersError = ref('')
const requiredDecimal = (label: string) => ({ validator: (_rule: unknown, value: string | null, callback: (error?: Error) => void) => value !== null && value !== '' && Number.isFinite(Number(value)) && Number(value) >= 0 ? callback() : callback(new Error(`请输入有效的${label}`)), trigger: 'blur' })
function truckRequired(label: string, trigger = 'blur') {
return { validator: (_rule: unknown, value: unknown, callback: (error?: Error) => void) => callback(isLooseboard.value || String(value || '').trim() ? undefined : new Error(`请填写${label}`)), trigger }
function demandRequired(label: string, trigger = 'blur') {
return { required: true, validator: (_rule: unknown, value: unknown, callback: (error?: Error) => void) => callback(String(value || '').trim() ? undefined : new Error(`请填写${label}`)), trigger }
}
const rules: FormRules<TransportFormModel> = {
transportMode: [{ validator: (_rule, value, callback) => (!isLooseboard.value || value) ? callback() : callback(new Error('请选择运输方式')), trigger: 'change' }],
palletRequired: [{ validator: (_rule, value, callback) => (!isLooseboard.value || value) ? callback() : callback(new Error('请选择是否打盘')), trigger: 'change' }],
transportType: [{ validator: (_rule, value, callback) => (!isLooseboard.value || value) ? callback() : callback(new Error('请选择运输类型')), trigger: 'change' }], origin: [{ required: true, message: '请输入起运地', trigger: 'blur' }], destination: [{ required: true, message: '请输入目的地', trigger: 'blur' }],
origin: [{ required: true, message: '请输入起运地', trigger: 'blur' }], destination: [{ required: true, message: '请输入目的地', trigger: 'blur' }],
packageCount: [{ required: true, type: 'number', min: 0, message: '件数不能小于 0', trigger: 'change' }], totalWeightKg: [requiredDecimal('总重量')], totalVolumeCbm: [requiredDecimal('总体积')],
vehicleType: [truckRequired('车辆类型', 'change')],
originContactName: [truckRequired('起运地联系人')], originContactPhone: [truckRequired('起运地联系电话')],
destinationContactName: [truckRequired('目的地联系人')], destinationContactPhone: [truckRequired('目的地联系电话')],
pickupAt: [truckRequired('取货时间', 'change')],
deliveryAt: [truckRequired('送货时间', 'change'), {
validator: (_rule, value, callback) => callback(!isLooseboard.value && value && form.pickupAt && value < form.pickupAt ? new Error('送货时间不能早于取货时间') : undefined), trigger: 'change',
vehicleType: [{ required: true, validator: (_rule, value, callback) => callback(isLooseboard.value || value ? undefined : new Error('请选择车辆类型')), trigger: 'change' }],
originContactName: [demandRequired('起运地联系人')], originContactPhone: [demandRequired('起运地联系电话')],
destinationContactName: [demandRequired('目的地联系人')], destinationContactPhone: [demandRequired('目的地联系电话')],
pickupAt: [{ required: true, validator: (_rule, value, callback) => callback(!value ? new Error('请选择取货时间') : undefined), trigger: 'change' }],
estimatedPalletCount: [{ type: 'number', min: 0, required: true, message: '预计托数不能小于 0', trigger: 'change' }],
deliveryAt: [demandRequired('送货时间', 'change'), {
validator: (_rule, value, callback) => callback(value && form.pickupAt && value < form.pickupAt ? new Error('送货时间不能早于取货时间') : undefined), trigger: 'change',
}],
budgetAmount: [{ pattern: /^\d+(\.\d{1,2})?$/, message: '预算总价最多保留 2 位小数', trigger: 'blur' }],
}
const transportTypes = [{ value: 'PORT_PICKUP', label: '⚓ 港口提柜' }, { value: 'WAREHOUSE_TO_WAREHOUSE', label: '🏭 仓到仓' }, { value: 'WAREHOUSE_TO_DOOR', label: '🚛 仓到门' }]
const transportModes = [{ value: 'SHARED_TRUCK', label: '🚐 拼车' }, { value: 'PRIORITY_CONSOLIDATION', label: '📦 优先拼载' }]
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: '已取消' }
const looseboardStatusLabels: Record<LocalLooseboardStatus, string> = { PENDING_ACCEPTANCE: '待接单', WAITING_INBOUND: '待入仓', RECEIVING: '收货中', WAITING_CONSOLIDATION: '待合托', PENDING_DISPATCH: '待派车', DISPATCHED: '已派车', IN_TRANSIT: '运输中', COMPLETED: '已完成', CANCELLED: '已取消' }
const statusLabel = computed(() => isLooseboard.value ? looseboardStatusLabels[form.status as LocalLooseboardStatus] : truckStatusLabels[form.status as LocalTransportStatus])
@@ -89,7 +85,7 @@ async function load() {
if (isLooseboard.value) {
const detail = await getLocalLooseboardOrder(orderId.value, routeCustomerId.value)
fleetArrangement.value = detail.fleetArrangement
Object.assign(form, { customerId: detail.customerId, orderNo: detail.orderNo, transportMode: detail.transportMode, palletRequired: detail.palletRequired, transportType: detail.transportType, origin: detail.origin, destination: detail.destination, originAddress: detail.originAddress, destinationAddress: detail.destinationAddress, contactName: detail.contactName, contactInfo: detail.contactInfo, containerType: detail.containerType, packageCount: detail.packageCount, totalWeightKg: detail.totalWeightKg, totalVolumeCbm: detail.totalVolumeCbm, budgetAmount: detail.budgetAmount, currency: detail.currency, requestedDate: detail.requestedDate, remark: detail.remark, status: detail.status, version: detail.version })
Object.assign(form, { customerId: detail.customerId, orderNo: detail.orderNo, productName: detail.productName || '', estimatedPalletCount: detail.estimatedPalletCount ?? 0, origin: detail.origin, destination: detail.destination, originAddress: detail.originAddress, destinationAddress: detail.destinationAddress, originContactName: detail.originContactName || '', originContactPhone: detail.originContactPhone || '', destinationContactName: detail.destinationContactName || '', destinationContactPhone: detail.destinationContactPhone || '', packageCount: detail.packageCount, totalWeightKg: detail.totalWeightKg, totalVolumeCbm: detail.totalVolumeCbm, budgetAmount: detail.budgetAmount, currency: detail.currency, pickupAt: detail.pickupAt || '', deliveryAt: detail.deliveryAt || '', remark: detail.remark, status: detail.status, version: detail.version })
} else {
const detail = await getUsTruckOrder(orderId.value, routeCustomerId.value)
fleetArrangement.value = detail.fleetArrangement
@@ -101,13 +97,16 @@ async function load() {
async function save() {
if (!editableStatus.value) { submitError.value = { message: `当前${domainLabel.value}订单状态不可编辑` }; return }
if (!editing.value && !form.customerId) { submitError.value = { message: '请选择客户' }; return }
if (isLooseboard.value && form.palletRequired !== 'YES' && form.palletRequired !== 'NO') { submitError.value = { message: '请选择是否打盘' }; return }
if (isLooseboard.value && form.pickupAt && form.deliveryAt && form.deliveryAt < form.pickupAt) { submitError.value = { message: '送达时间不能早于取货时间' }; return }
for (const [value, label] of [[form.originContactName, '起运地联系人'], [form.originContactPhone, '起运地联系电话'], [form.destinationContactName, '目的地联系人'], [form.destinationContactPhone, '目的地联系电话'], [form.pickupAt, '取货时间'], [form.deliveryAt, '送货时间']]) {
if (!value.trim()) { submitError.value = { message: `请填写${label}` }; return }
}
if (!await formRef.value?.validate().catch(() => false)) return
const common = { origin: form.origin.trim(), originAddress: optional(form.originAddress), destination: form.destination.trim(), destinationAddress: optional(form.destinationAddress), packageCount: form.packageCount, totalWeightKg: Number(form.totalWeightKg), totalVolumeCbm: Number(form.totalVolumeCbm), budgetAmount: amount(form.budgetAmount), currency: form.currency, remark: optional(form.remark) }
saving.value = true; submitError.value = null
try {
if (isLooseboard.value) {
const loose = { ...common, transportType: form.transportType as LocalTransportType, containerType: optional(form.containerType), contactName: optional(form.contactName), contactInfo: optional(form.contactInfo), requestedDate: form.requestedDate, transportMode: form.transportMode as LooseboardTransportMode, palletRequired: form.palletRequired as PalletRequired }
const loose = { ...common, productName: optional(form.productName), estimatedPalletCount: form.estimatedPalletCount, originContactName: form.originContactName.trim(), originContactPhone: form.originContactPhone.trim(), destinationContactName: form.destinationContactName.trim(), destinationContactPhone: form.destinationContactPhone.trim(), pickupAt: form.pickupAt, deliveryAt: form.deliveryAt }
if (editing.value) await updateLocalLooseboardOrder(orderId.value, routeCustomerId.value, { ...loose, version: form.version } satisfies UpdateLocalLooseboardOrderRequest)
else await createLocalLooseboardOrder(form.customerId, { ...loose, idempotencyKey: form.idempotencyKey } satisfies CreateLocalLooseboardOrderRequest)
} else {
@@ -128,12 +127,43 @@ watch([orderId, routeCustomerId, isLooseboard], () => { void load(); void loadCu
<LoadErrorState v-if="loadError" :title="`${domainLabel}订单暂时无法加载`" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState>
<FleetArrangementSummary v-else-if="fleetArrangement" :arrangement="fleetArrangement" />
<section v-else v-loading="loading" class="form-panel"><el-form class="density-form" ref="formRef" :model="form" :rules="rules" label-position="top" :disabled="saving || !editableStatus">
<FormSection class="density-form-section" v-if="!editing" title="客户信息"><el-form-item label="客户"><el-select v-model="form.customerId" filterable :loading="customersLoading" :disabled="Boolean(customersError)" placeholder="请选择客户" style="width:100%"><el-option v-for="item in customers" :key="String(item.id)" :label="item.tenantName" :value="String(item.id)" /></el-select><small v-if="customersError" class="field-error">{{ customersError }}</small></el-form-item></FormSection>
<FormSection class="density-form-section" v-if="!editing" title="客户信息"><el-form-item label="客户" required><el-select v-model="form.customerId" filterable :loading="customersLoading" :disabled="Boolean(customersError)" placeholder="请选择客户" style="width:100%"><el-option v-for="item in customers" :key="String(item.id)" :label="item.tenantName" :value="String(item.id)" /></el-select><small v-if="customersError" class="field-error">{{ customersError }}</small></el-form-item></FormSection>
<el-alert v-if="editing && !editableStatus" type="warning" :closable="false" show-icon title="当前状态不可普通编辑,请返回列表查看" />
<FormSection class="density-form-section" v-if="isLooseboard" title="散板方式"><div class="choice-block"><el-form-item label="运输方式" prop="transportMode"><el-radio-group v-model="form.transportMode"><el-radio-button v-for="item in transportModes" :key="item.value" :value="item.value">{{ item.label }}</el-radio-button></el-radio-group></el-form-item><el-form-item label="是否打盘" prop="palletRequired"><el-radio-group v-model="form.palletRequired"><el-radio-button value="YES">是</el-radio-button><el-radio-button value="NO">否</el-radio-button></el-radio-group></el-form-item></div></FormSection>
<FormSection class="density-form-section" title="运输信息"><el-form-item v-if="isLooseboard" label="运输类型" prop="transportType"><el-radio-group v-model="form.transportType"><el-radio-button v-for="item in transportTypes" :key="item.value" :value="item.value">{{ item.label }}</el-radio-button></el-radio-group></el-form-item><el-form-item v-if="!isLooseboard" label="车辆类型" prop="vehicleType" required><el-select v-model="form.vehicleType" placeholder="请选择车辆类型"><el-option label="15拖" value="PALLET_15" /><el-option label="30拖" value="PALLET_30" /></el-select></el-form-item><div class="form-grid two"><el-form-item label="起运地" prop="origin"><el-input v-model="form.origin" maxlength="255" placeholder="城市 / 港口 / 州" /></el-form-item><el-form-item label="目的地" prop="destination"><el-input v-model="form.destination" maxlength="255" placeholder="城市 / 州" /></el-form-item><el-form-item label="详细地址"><el-input v-model="form.originAddress" maxlength="500" placeholder="街道 / 门牌" /></el-form-item><el-form-item label="详细地址 / 码头 / 仓号"><el-input v-model="form.destinationAddress" maxlength="500" placeholder="地址 / 码头 / 仓号" /></el-form-item><el-form-item v-if="isLooseboard" label="联系人" prop="contactName"><el-input v-model="form.contactName" maxlength="128" placeholder="请输入联系人姓名" /></el-form-item><el-form-item v-if="isLooseboard" label="联系方式" prop="contactInfo"><el-input v-model="form.contactInfo" maxlength="128" placeholder="请输入联系方式" /></el-form-item><el-form-item v-if="!isLooseboard" label="起运地联系人" prop="originContactName" required><el-input v-model.trim="form.originContactName" maxlength="128" placeholder="请输入起运地联系人" /></el-form-item><el-form-item v-if="!isLooseboard" label="目的地联系人" prop="destinationContactName" required><el-input v-model.trim="form.destinationContactName" maxlength="128" placeholder="请输入目的地联系人" /></el-form-item><el-form-item v-if="!isLooseboard" label="起运地联系电话" prop="originContactPhone" required><el-input v-model.trim="form.originContactPhone" maxlength="128" placeholder="请输入起运地联系电话" type="tel" /></el-form-item><el-form-item v-if="!isLooseboard" label="目的地联系电话" prop="destinationContactPhone" required><el-input v-model.trim="form.destinationContactPhone" maxlength="128" placeholder="请输入目的地联系电话" type="tel" /></el-form-item></div></FormSection>
<FormSection class="density-form-section" title="📦 货物信息"><div class="form-grid" :class="isLooseboard ? 'four' : 'three'"><el-form-item v-if="isLooseboard" label="柜型"><el-select v-model="form.containerType"><el-option v-for="item in CONTAINER_SPECIFICATIONS" :key="item.type" :label="item.type" :value="item.type" /></el-select></el-form-item><el-form-item label="件数" prop="packageCount"><el-input-number v-model="form.packageCount" :min="0" controls-position="right" placeholder="请输入件数" /></el-form-item><el-form-item label="总重量 (kg)" prop="totalWeightKg"><el-input v-model="form.totalWeightKg" inputmode="decimal" placeholder="请输入总重量" /></el-form-item><el-form-item label="总体积 (m³)" prop="totalVolumeCbm"><el-input v-model="form.totalVolumeCbm" inputmode="decimal" placeholder="请输入总体积" /></el-form-item></div></FormSection>
<FormSection class="density-form-section" title="费用与时效"><div class="form-grid two"><el-form-item label="预算总价" prop="budgetAmount"><el-input v-model="form.budgetAmount" inputmode="decimal" placeholder="请输入预算金额" ><template #append>{{ form.currency }}</template></el-input></el-form-item><el-form-item v-if="isLooseboard" label="要求时间" prop="requestedDate"><el-date-picker v-model="form.requestedDate" type="date" value-format="YYYY-MM-DD" clearable /></el-form-item><el-form-item v-if="!isLooseboard" label="取货时间" prop="pickupAt" required><el-date-picker v-model="form.pickupAt" type="datetime" format="YYYY-MM-DD HH:mm" time-format="HH:mm" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择取货时间" @change="formRef?.validateField('deliveryAt').catch(() => undefined)" /></el-form-item><el-form-item v-if="!isLooseboard" label="送货时间" prop="deliveryAt" required><el-date-picker v-model="form.deliveryAt" type="datetime" format="YYYY-MM-DD HH:mm" time-format="HH:mm" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择送货时间" /></el-form-item><el-form-item v-if="editing" label="状态"><el-input :model-value="statusLabel" disabled /></el-form-item><el-form-item class="span-2" label="备注"><el-input v-model="form.remark" type="textarea" :rows="3" maxlength="1000" show-word-limit placeholder="请输入备注(选填)" /></el-form-item></div></FormSection>
<template v-if="isLooseboard">
<FormSection class="density-form-section" title="运输信息">
<div class="form-grid two">
<el-form-item label="取货地" prop="origin"><el-input v-model.trim="form.origin" maxlength="255" placeholder="城市 / 州 / 详细地址" /></el-form-item>
<el-form-item label="目的地" prop="destination"><el-input v-model.trim="form.destination" maxlength="255" placeholder="城市 / 州 / 详细地址" /></el-form-item>
<el-form-item label="起运地联系人" prop="originContactName" required><el-input v-model.trim="form.originContactName" maxlength="128" placeholder="请输入起运地联系人" /></el-form-item>
<el-form-item label="起运地联系电话" prop="originContactPhone" required><el-input v-model.trim="form.originContactPhone" maxlength="128" placeholder="请输入起运地联系电话" type="tel" /></el-form-item>
<el-form-item label="目的地联系人" prop="destinationContactName" required><el-input v-model.trim="form.destinationContactName" maxlength="128" placeholder="请输入目的地联系人" /></el-form-item>
<el-form-item label="目的地联系电话" prop="destinationContactPhone" required><el-input v-model.trim="form.destinationContactPhone" maxlength="128" placeholder="请输入目的地联系电话" type="tel" /></el-form-item>
</div>
</FormSection>
<FormSection class="density-form-section" title="货物信息"><div class="form-grid four">
<el-form-item label="商品名称"><el-input v-model.trim="form.productName" maxlength="255" placeholder="请输入商品名称(选填)" /></el-form-item>
<el-form-item label="预计托数" prop="estimatedPalletCount"><el-input-number v-model="form.estimatedPalletCount" :min="0" :precision="0" controls-position="right" placeholder="请输入预计托数" /></el-form-item>
<el-form-item label="总重量 (kg)" prop="totalWeightKg"><el-input v-model="form.totalWeightKg" inputmode="decimal" placeholder="请输入总重量" /></el-form-item>
<el-form-item label="总体积 (m³)" prop="totalVolumeCbm"><el-input v-model="form.totalVolumeCbm" inputmode="decimal" placeholder="请输入总体积" /></el-form-item>
</div></FormSection>
<FormSection class="density-form-section" title="费用与时效"><div class="form-grid two">
<el-form-item label="预算总价" prop="budgetAmount"><el-input v-model="form.budgetAmount" inputmode="decimal" placeholder="选填"><template #append><el-select v-model="form.currency" aria-label="预算币种" class="budget-currency"><el-option label="USD" value="USD" /><el-option label="CNY" value="CNY" /></el-select></template></el-input></el-form-item>
<div></div>
<el-form-item label="取货时间" prop="pickupAt" required><el-date-picker v-model="form.pickupAt" type="datetime" format="YYYY-MM-DD HH:mm" time-format="HH:mm" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择取货时间" @change="formRef?.validateField('deliveryAt').catch(() => undefined)" /></el-form-item>
<el-form-item label="送货时间" prop="deliveryAt" required><el-date-picker v-model="form.deliveryAt" type="datetime" format="YYYY-MM-DD HH:mm" time-format="HH:mm" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择送货时间" /></el-form-item>
</div></FormSection>
<el-collapse><el-collapse-item title="补充信息(选填)" name="extra"><div class="form-grid two">
<el-form-item label="件数" prop="packageCount"><el-input-number v-model="form.packageCount" :min="0" :precision="0" controls-position="right" placeholder="请输入件数" /></el-form-item>
<el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="2" maxlength="1000" placeholder="请输入备注(选填)" /></el-form-item>
<el-form-item label="取货地址补充"><el-input v-model="form.originAddress" maxlength="500" placeholder="街道 / 门牌等补充信息(选填)" /></el-form-item>
<el-form-item label="目的地址补充"><el-input v-model="form.destinationAddress" maxlength="500" placeholder="街道 / 门牌等补充信息(选填)" /></el-form-item>
</div></el-collapse-item></el-collapse>
</template>
<template v-else>
<FormSection class="density-form-section" title="运输信息"><el-form-item v-if="!isLooseboard" label="车辆类型" prop="vehicleType" required><el-select v-model="form.vehicleType" placeholder="请选择车辆类型"><el-option label="15拖" value="PALLET_15" /><el-option label="30拖" value="PALLET_30" /></el-select></el-form-item><div class="form-grid two"><el-form-item label="起运地" prop="origin"><el-input v-model="form.origin" maxlength="255" placeholder="城市 / 港口 / 州" /></el-form-item><el-form-item label="目的地" prop="destination"><el-input v-model="form.destination" maxlength="255" placeholder="城市 / 州" /></el-form-item><el-form-item label="详细地址"><el-input v-model="form.originAddress" maxlength="500" placeholder="街道 / 门牌" /></el-form-item><el-form-item label="详细地址 / 码头 / 仓号"><el-input v-model="form.destinationAddress" maxlength="500" placeholder="地址 / 码头 / 仓号" /></el-form-item><el-form-item v-if="!isLooseboard" label="起运地联系人" prop="originContactName" required><el-input v-model.trim="form.originContactName" maxlength="128" placeholder="请输入起运地联系人" /></el-form-item><el-form-item v-if="!isLooseboard" label="目的地联系人" prop="destinationContactName" required><el-input v-model.trim="form.destinationContactName" maxlength="128" placeholder="请输入目的地联系人" /></el-form-item><el-form-item v-if="!isLooseboard" label="起运地联系电话" prop="originContactPhone" required><el-input v-model.trim="form.originContactPhone" maxlength="128" placeholder="请输入起运地联系电话" type="tel" /></el-form-item><el-form-item v-if="!isLooseboard" label="目的地联系电话" prop="destinationContactPhone" required><el-input v-model.trim="form.destinationContactPhone" maxlength="128" placeholder="请输入目的地联系电话" type="tel" /></el-form-item></div></FormSection>
<FormSection class="density-form-section" title="📦 货物信息"><div class="form-grid" :class="isLooseboard ? 'four' : 'three'"><el-form-item label="件数" prop="packageCount"><el-input-number v-model="form.packageCount" :min="0" controls-position="right" placeholder="请输入件数" /></el-form-item><el-form-item label="总重量 (kg)" prop="totalWeightKg"><el-input v-model="form.totalWeightKg" inputmode="decimal" placeholder="请输入总重量" /></el-form-item><el-form-item label="总体积 (m³)" prop="totalVolumeCbm"><el-input v-model="form.totalVolumeCbm" inputmode="decimal" placeholder="请输入总体积" /></el-form-item></div></FormSection>
<FormSection class="density-form-section" title="费用与时效"><div class="form-grid two"><el-form-item label="预算总价" prop="budgetAmount"><el-input v-model="form.budgetAmount" inputmode="decimal" placeholder="请输入预算金额" ><template #append>{{ form.currency }}</template></el-input></el-form-item><el-form-item v-if="!isLooseboard" label="取货时间" prop="pickupAt" required><el-date-picker v-model="form.pickupAt" type="datetime" format="YYYY-MM-DD HH:mm" time-format="HH:mm" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择取货时间" @change="formRef?.validateField('deliveryAt').catch(() => undefined)" /></el-form-item><el-form-item v-if="!isLooseboard" label="送货时间" prop="deliveryAt" required><el-date-picker v-model="form.deliveryAt" type="datetime" format="YYYY-MM-DD HH:mm" time-format="HH:mm" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择送货时间" /></el-form-item><el-form-item v-if="editing" label="状态"><el-input :model-value="statusLabel" disabled /></el-form-item><el-form-item class="span-2" label="备注"><el-input v-model="form.remark" type="textarea" :rows="3" maxlength="1000" show-word-limit placeholder="请输入备注(选填)" /></el-form-item></div></FormSection>
</template>
</el-form><FormSection v-if="!isLooseboard" class="density-form-section" title="车辆与派车"><el-button @click="routeOpen = true">查看路线</el-button></FormSection><footer class="form-actions"><el-button @click="back">取消</el-button><el-button type="primary" :loading="saving" :disabled="!editableStatus" @click="save"><Save :size="16" />{{ editing ? '保存修改' : '提交订单' }}</el-button></footer></section>
</DetailPageLayout><TransportRouteDialog v-model="routeOpen" :origin="routeLocation(form.origin, form.originAddress)" :destinations="[routeLocation(form.destination, form.destinationAddress)]" /></AdminPage>
</template>
@@ -142,6 +172,6 @@ watch([orderId, routeCustomerId, isLooseboard], () => { void load(); void loadCu
.form-grid.two { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.form-grid.four { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.form-grid.three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.choice-block { display: grid; grid-template-columns: 1fr 1fr; gap: var(--admin-space-5); }
.overseas-business-editor .form-grid .budget-currency { width: 85px; }
.span-2 { grid-column: 1 / -1; }
</style>