Merge branch 'release/test' of http://8.163.30.128:10082/html/client-web into release/test

This commit is contained in:
2026-10-19 14:53:59 +08:00
4 changed files with 224 additions and 35 deletions
@@ -0,0 +1,11 @@
# 本土卡车字段调整核对
依据:用户提供的下单页截图 `codex-clipboard-c458c2e1-e9a8-4726-a8cd-4581e5e7102f.png` 及本轮字段、必填和时分确认。当前需求规则见 `docs/海外仓业务/README.md`“美国本土卡独立下单”。
| 页面 | 操作 | 依据及承载 | 当前调整 | 验收 |
|---|---|---|---|---|
| 美国本土卡 | 新增 | 用户截图:独立页面、单列表单、取消和提交按钮 | 保留容器、按钮;按确认删除字段,新增车型及两端联系人和时间 | 自动验证与人工视觉验收分开;待人工截图确认 |
| 美国本土卡 | 列表 | 现有 `BusinessOrdersView.vue` 的独立列表与左侧操作列 | 对应列展示车型和取送时间 | 待人工截图确认 |
| 美国本土卡 | 详情 | 现有独立详情路由及键值网格 | 展示保存的两端联系信息、车型和取送时间;加宽长字段标签 | 待人工截图确认 |
不改动散板字段和承载方式。无新增审核、删除或附件操作。
+46 -8
View File
@@ -18,6 +18,9 @@ export interface BusinessOrderQuery {
routeKeyword?: string
transportType?: string
transportMode?: string
vehicleType?: 'PALLET_15' | 'PALLET_30'
pickupFrom?: string
pickupTo?: string
requestedFrom?: string
requestedTo?: string
createdFrom?: string
@@ -27,21 +30,28 @@ export interface BusinessOrderQuery {
export interface TransportSummary {
fleetArrangement: import('../fleetArrangement').FleetArrangement | null
orderNo: string
transportType: string
transportType?: string
transportMode?: string
palletRequired?: string | null
origin: string
destination: string
containerType: string | null
containerType?: string | null
palletCount?: number
packageCount: number
totalWeightKg: number
totalVolumeCbm: number
budgetAmount: number | null
currency: string | null
contactName: string | null
contactInfo: string | null
requestedDate: string | null
contactName?: string | null
contactInfo?: string | null
requestedDate?: string | null
vehicleType?: 'PALLET_15' | 'PALLET_30' | null
originContactName?: string | null
originContactPhone?: string | null
destinationContactName?: string | null
destinationContactPhone?: string | null
pickupAt?: string | null
deliveryAt?: string | null
status: string
version: number
createdAt: string
@@ -64,7 +74,6 @@ export interface TransportCreate {
destination: string
destinationAddress?: string
containerType?: string
palletCount?: number
packageCount: number
totalWeightKg: number
totalVolumeCbm: number
@@ -77,10 +86,35 @@ export interface TransportCreate {
idempotencyKey: string
}
export interface TruckCreate {
vehicleType: 'PALLET_15' | 'PALLET_30'
origin: string
originAddress?: string
destination: string
destinationAddress?: string
originContactName: string
originContactPhone: string
destinationContactName: string
destinationContactPhone: string
pickupAt: string
deliveryAt: string
packageCount: number
totalWeightKg: number
totalVolumeCbm: number
budgetAmount?: number
currency?: string
remark?: string
idempotencyKey: string
}
export interface TransportUpdate extends Omit<TransportCreate, 'idempotencyKey'> {
version: number
}
export interface TruckUpdate extends Omit<TruckCreate, 'idempotencyKey'> {
version: number
}
const endpointByKind: Record<CrossBorderBusinessKind, string> = {
trucking: '/merchant/v1/last-leg/us-truck-orders',
looseboard: '/merchant/v1/last-leg/local-looseboard-orders',
@@ -98,11 +132,15 @@ export function getBusinessOrder<T>(kind: CrossBorderBusinessKind, orderNo: stri
return apiRequest<T>({ method: 'GET', url: `${endpointByKind[kind]}/${encodeURIComponent(orderNo)}` })
}
export function createTransport(kind: 'trucking' | 'looseboard', payload: TransportCreate) {
export function createTransport(kind: 'trucking', payload: TruckCreate): Promise<TransportDetail>
export function createTransport(kind: 'looseboard', payload: TransportCreate): Promise<TransportDetail>
export function createTransport(kind: CrossBorderBusinessKind, payload: TransportCreate | TruckCreate) {
return apiRequest<TransportDetail>({ method: 'POST', url: endpointByKind[kind], data: payload })
}
export function updateTransport(kind: 'trucking' | 'looseboard', orderNo: string, payload: TransportUpdate) {
export function updateTransport(kind: 'trucking', orderNo: string, payload: TruckUpdate): Promise<TransportDetail>
export function updateTransport(kind: 'looseboard', orderNo: string, payload: TransportUpdate): Promise<TransportDetail>
export function updateTransport(kind: CrossBorderBusinessKind, orderNo: string, payload: TransportUpdate | TruckUpdate) {
return apiRequest<TransportDetail>({
method: kind === 'trucking' ? 'PATCH' : 'PUT',
url: `${endpointByKind[kind]}/${encodeURIComponent(orderNo)}`,
@@ -115,3 +115,65 @@ test('uses the shared dense order-list and detail visual language', () => {
assert.match(view, /\.detail-grid \{[^}]*grid-template-columns: repeat\(4, minmax\(0, 1fr\)\)/)
assert.match(view, /\.detail-grid div \{[^}]*min-height: 34px;[^}]*grid-template-columns: 76px minmax\(0, 1fr\)/)
})
function transportHarness(kind = 'trucking', failFirst = false) {
const calls = []
const script = view.match(/<script setup lang="ts">([\s\S]*?)<\/script>/)[1]
const compiled = ts.transpileModule(script, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext } }).outputText
.replace(/^import .*?;$/gm, '').replace(/^export \{\};$/gm, '')
const harness = runInNewContext(`${compiled}\n({ form, rules, formRef, submit, detail, detailFields })`, {
defineProps: () => ({ kind, createPage: true, detailPage: false }),
withDefaults: props => props,
computed: getter => ({ get value() { return typeof getter === 'function' ? getter() : getter.get() } }),
reactive: value => value, ref: value => ({ value }), onMounted: () => {}, watch: () => {},
useAuthStore: () => ({ hasPermission: () => true }), useRouter: () => ({ push: async () => {} }), useRoute: () => ({ params: {} }),
DEFAULT_CONTAINER_TYPE: '40HQ', poolOrderStatusLabels: {},
ElMessage: { success: () => {}, error: () => {} },
createTransport: async (business, payload) => { calls.push({ business, payload: { ...payload } }); if (failFirst && calls.length === 1) throw new Error('retry'); },
})
harness.formRef.value = { validate: async () => true, clearValidate: () => {} }
return { ...harness, calls }
}
test('truck requires both contacts vehicle and minute times without inheriting looseboard fields', () => {
const truck = transportHarness()
for (const key of ['vehicleType', 'originContactName', 'originContactPhone', 'destinationContactName', 'destinationContactPhone', 'pickupAt', 'deliveryAt']) assert.equal(truck.rules.value[key][0].required, true)
for (const key of ['palletCount', 'contactName', 'contactInfo', 'requestedDate']) assert.equal(truck.rules.value[key], undefined)
const loose = transportHarness('looseboard')
assert.equal(loose.rules.value.contactName, undefined)
assert.equal(loose.rules.value.contactInfo, undefined)
assert.equal(loose.rules.value.transportMode[0].required, true)
truck.form.pickupAt = '2026-10-20T09:30:00'
let failure
truck.rules.value.deliveryAt[1].validator({}, '2026-10-20T09:29:00', error => { failure = error })
assert.equal(failure.message, '送货时间不能早于取货时间')
})
test('truck retry submits the complete new contract with identical local times and idempotency key', async () => {
const current = transportHarness('trucking', true)
Object.assign(current.form, { vehicleType: 'PALLET_30', origin: 'Los Angeles', destination: 'Ontario', originContactName: 'A', originContactPhone: '+1-555-0100', destinationContactName: 'B', destinationContactPhone: '+1-555-0101', pickupAt: '2026-10-20T09:30:00', deliveryAt: '2026-10-20T15:45:00' })
await current.submit()
assert.equal(current.form.destinationContactName, 'B')
await current.submit()
assert.equal(current.calls.length, 2)
const first = current.calls[0].payload
const second = current.calls[1].payload
assert.equal(first.idempotencyKey, second.idempotencyKey)
assert.equal(second.vehicleType, 'PALLET_30')
assert.equal(second.originContactPhone, '+1-555-0100')
assert.equal(second.destinationContactName, 'B')
assert.equal(second.pickupAt, '2026-10-20T09:30:00')
assert.equal(second.deliveryAt, '2026-10-20T15:45:00')
for (const field of ['transportType', 'containerType', 'palletCount', 'contactName', 'contactInfo', 'requestedDate']) assert.equal(Object.hasOwn(second, field), false)
})
test('truck details render saved minute times and unknown pool demand without timezone conversion', () => {
const current = transportHarness()
current.detail.value = { summary: { vehicleType: null, pickupAt: '2026-10-20T09:30:00', deliveryAt: null }, originAddress: null, destinationAddress: null, remark: null }
const fields = new Map(current.detailFields.value)
assert.equal(fields.get('车辆类型'), '—')
assert.equal(fields.get('取货时间'), '2026-10-20 09:30')
assert.equal(fields.get('送货时间'), '—')
assert.equal(fields.has('柜型'), false)
assert.equal(fields.has('托盘数'), false)
})
@@ -44,18 +44,25 @@ interface BusinessForm {
destination: string
destinationAddress: string
containerType: string
palletCount: number
packageCount: number
totalWeightKg: number
totalVolumeCbm: number
budgetAmount?: number
currency: string
vehicleType: '' | 'PALLET_15' | 'PALLET_30'
originContactName: string
originContactPhone: string
destinationContactName: string
destinationContactPhone: string
pickupAt: string
deliveryAt: string
contactName: string
contactInfo: string
requestedDate: string
remark: string
cargoName: string
deliveryDate: string
palletCount: number
}
const configuration = computed(() => ({
@@ -75,13 +82,15 @@ function transportColumns(looseboard: boolean): ColumnDefinition[] {
return [
{ key: 'orderNo', label: '订单编号', width: 180 },
...(looseboard ? [{ key: 'transportMode', label: '运输方式', width: 120 }, { key: 'palletRequired', label: '是否打盘', width: 100 }] : []),
{ key: 'transportType', label: '运输类型', width: 130 },
...(looseboard ? [{ key: 'transportType', label: '运输类型', width: 130 }] : [{ key: 'vehicleType', label: '车辆类型', width: 110 }]),
{ key: 'origin', label: '起运地', width: 150 },
{ key: 'destination', label: '目的地', width: 150 },
{ key: 'containerType', label: '柜型', width: 100 },
...(looseboard ? [{ key: 'containerType', label: '柜型', width: 100 }] : []),
{ key: 'totalWeightKg', label: '总重量(kg)', width: 120 },
{ key: 'budgetAmount', label: '预算总价', width: 110 },
{ key: 'requestedDate', label: '要求时间', width: 120 },
...(looseboard ? [{ key: 'requestedDate', label: '要求时间', width: 120 }] : [
{ key: 'pickupAt', label: '取货时间', width: 165 }, { key: 'deliveryAt', label: '送货时间', width: 165 },
]),
{ key: 'status', label: '状态', width: 130 },
{ key: 'fleetArrangement', label: '承运安排', width: 190 },
]
@@ -139,29 +148,48 @@ const queryFields = computed<QueryField[]>(() => [
])
const form = reactive<BusinessForm>(emptyForm())
const rules: FormRules<BusinessForm> = {
const rules = computed<FormRules<BusinessForm>>(() => ({
origin: [{ required: true, message: '请输入起运地', trigger: 'blur' }],
destination: [{ required: true, message: '请输入目的地', trigger: 'blur' }],
palletCount: [{ required: true, type: 'number', min: 1, message: '托盘数至少为 1', trigger: 'change' }],
contactName: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
contactInfo: [{ required: true, message: '请输入联系电话或联系方式', trigger: 'blur' }],
requestedDate: [{ required: true, message: '请选择要求时间', trigger: 'change' }],
}
...(props.kind === 'trucking' ? {
vehicleType: [{ required: true, message: '请选择车辆类型', trigger: 'change' }],
originContactName: [{ required: true, message: '请输入起运地联系人', trigger: 'blur' }],
originContactPhone: [{ required: true, message: '请输入起运地联系电话', trigger: 'blur' }],
destinationContactName: [{ required: true, message: '请输入目的地联系人', trigger: 'blur' }],
destinationContactPhone: [{ required: true, message: '请输入目的地联系电话', trigger: 'blur' }],
pickupAt: [{ required: true, message: '请选择取货时间', trigger: 'change' }],
deliveryAt: [{ required: true, message: '请选择送货时间', trigger: 'change' }, {
validator: (_rule: unknown, value: string, callback: (error?: Error) => void) => {
callback(value && form.pickupAt && value < form.pickupAt ? new Error('送货时间不能早于取货时间') : undefined)
}, trigger: 'change',
}],
} : {
contactName: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
contactInfo: [{ required: true, message: '请输入联系电话或联系方式', trigger: 'blur' }],
}),
}))
const detailFields = computed(() => {
if (!detail.value) return []
const summary = detail.value.summary
const current = detail.value as TransportDetail
return [
['运输方式', transportModeLabel(read(summary, 'transportMode'))],
['是否打盘', yesNo(read(summary, 'palletRequired'))], ['运输类型', transportTypeLabel(read(summary, 'transportType'))],
...(props.kind === 'looseboard' ? [
['运输方式', transportModeLabel(read(summary, 'transportMode'))], ['是否打盘', yesNo(read(summary, 'palletRequired'))],
['运输类型', transportTypeLabel(read(summary, 'transportType'))],
] : [['车辆类型', vehicleTypeLabel(summary.vehicleType)]]),
['起运地', read(summary, 'origin')], ['起运地址', current.originAddress],
['目的地', read(summary, 'destination')], ['目的地址', current.destinationAddress],
['柜型', read(summary, 'containerType')], ['托盘数', read(summary, 'palletCount')],
...(props.kind === 'looseboard' ? [['柜型', read(summary, 'containerType')]] : []),
['件数', read(summary, 'packageCount')], ['总重量(kg)', read(summary, 'totalWeightKg')],
['总体积(m³)', read(summary, 'totalVolumeCbm')], ['预算总价', money(read(summary, 'budgetAmount'), read(summary, 'currency'))],
['联系人', read(summary, 'contactName')], ['联系方式', read(summary, 'contactInfo')],
['要求时间', read(summary, 'requestedDate')], ['备注', current.remark],
...(props.kind === 'looseboard' ? [
['联系人', summary.contactName], ['联系方式', summary.contactInfo], ['要求时间', summary.requestedDate],
] : [
['起运地联系人', summary.originContactName], ['起运地联系电话', summary.originContactPhone],
['目的地联系人', summary.destinationContactName], ['目的地联系电话', summary.destinationContactPhone],
['取货时间', localMinute(summary.pickupAt)], ['送货时间', localMinute(summary.deliveryAt)],
]), ['备注', current.remark],
]
})
@@ -180,9 +208,9 @@ const detailStatus = computed(() => detail.value ? statusLabel(detail.value.summ
function emptyForm(): BusinessForm {
return {
transportMode: 'SHARED_TRUCK', palletRequired: 'NO', transportType: 'PORT_PICKUP', origin: '', originAddress: '',
destination: '', destinationAddress: '', containerType: DEFAULT_CONTAINER_TYPE, palletCount: 1, packageCount: 0,
destination: '', destinationAddress: '', containerType: DEFAULT_CONTAINER_TYPE, packageCount: 0,
totalWeightKg: 0, totalVolumeCbm: 0, currency: 'USD', contactName: '', contactInfo: '', requestedDate: '',
remark: '', cargoName: '', deliveryDate: '',
remark: '', cargoName: '', deliveryDate: '', palletCount: 0, vehicleType: '', originContactName: '', originContactPhone: '', destinationContactName: '', destinationContactPhone: '', pickupAt: '', deliveryAt: '',
}
}
@@ -211,6 +239,8 @@ function text(value: unknown) { return value === null || value === undefined ||
function money(amount: unknown, currency: unknown) { return amount === null || amount === undefined ? '—' : `${text(currency)} ${Number(amount).toFixed(2)}` }
function dateTime(value: unknown) { return value ? new Date(String(value)).toLocaleString('zh-CN', { hour12: false }) : '—' }
function yesNo(value: unknown) { return value === 'YES' ? '是' : value === 'NO' ? '否' : text(value) }
function vehicleTypeLabel(value: unknown) { return ({ PALLET_15: '15拖', PALLET_30: '30拖' } as Record<string, string>)[String(value)] || text(value) }
function localMinute(value: unknown) { return value ? String(value).replace('T', ' ').slice(0, 16) : '—' }
function transportTypeLabel(value: unknown) { return ({ PORT_PICKUP: '港口提柜', WAREHOUSE_TO_WAREHOUSE: '仓到仓', WAREHOUSE_TO_DOOR: '仓到门' } as Record<string, string>)[String(value)] || text(value) }
function transportModeLabel(value: unknown) { return ({ SHARED_TRUCK: '拼车', PRIORITY_CONSOLIDATION: '优先拼载' } as Record<string, string>)[String(value)] || text(value) }
function statusLabel(value: unknown) {
@@ -224,6 +254,8 @@ function displayCell(candidate: unknown, key: string) {
const row = candidate as BusinessRow
const value = read(row, key)
if (key === 'status') return statusLabel(value)
if (key === 'vehicleType') return vehicleTypeLabel(value)
if (key === 'pickupAt' || key === 'deliveryAt') return localMinute(value)
if (key === 'transportType') return transportTypeLabel(value)
if (key === 'transportMode') return transportModeLabel(value)
if (key === 'palletRequired') return yesNo(value)
@@ -310,18 +342,27 @@ async function submit() {
submitKey.value = stableSubmitKey
try {
const base = {
transportType: form.transportType, origin: form.origin, originAddress: form.originAddress || undefined,
origin: form.origin, originAddress: form.originAddress || undefined,
destination: form.destination, destinationAddress: form.destinationAddress || undefined,
containerType: form.containerType || undefined, packageCount: form.packageCount,
packageCount: form.packageCount,
totalWeightKg: form.totalWeightKg, totalVolumeCbm: form.totalVolumeCbm,
budgetAmount: form.budgetAmount, currency: form.currency || undefined,
contactName: form.contactName || undefined, contactInfo: form.contactInfo || undefined,
requestedDate: form.requestedDate || undefined, remark: [form.cargoName, form.remark].filter(Boolean).join(' · ') || undefined,
remark: props.kind === 'looseboard' ? [form.cargoName, form.remark].filter(Boolean).join(' · ') || undefined : form.remark || undefined,
idempotencyKey: stableSubmitKey,
}
await createTransport(props.kind, props.kind === 'trucking'
? { ...base, palletCount: form.palletCount }
: { ...base, transportMode: form.transportMode, palletRequired: form.palletRequired })
if (props.kind === 'trucking') {
if (!form.vehicleType) return
await createTransport('trucking', { ...base, vehicleType: form.vehicleType,
originContactName: form.originContactName, originContactPhone: form.originContactPhone,
destinationContactName: form.destinationContactName, destinationContactPhone: form.destinationContactPhone,
pickupAt: form.pickupAt, deliveryAt: form.deliveryAt,
})
} else {
await createTransport('looseboard', { ...base, transportMode: form.transportMode, palletRequired: form.palletRequired,
transportType: form.transportType, containerType: form.containerType || undefined,
contactName: form.contactName || undefined, contactInfo: form.contactInfo || undefined, requestedDate: form.requestedDate || undefined,
})
}
ElMessage.success('订单提交成功')
resetForm()
if (props.createPage) await router.push({ name: listRouteName.value })
@@ -365,7 +406,7 @@ onMounted(() => { if (props.detailPage) void loadDetail(); else if (!props.creat
<div v-if="error" class="business-detail-error" role="alert"><strong>{{ error }}</strong><el-button size="small" @click="loadDetail">重新加载</el-button></div>
<section v-else class="business-detail-section">
<h3>订单信息</h3>
<dl class="detail-grid">
<dl class="detail-grid" :class="{ 'trucking-detail-grid': kind === 'trucking' }">
<div v-for="field in detailFields" :key="String(field[0])" :class="detailFieldClass(field[0])"><dt>{{ field[0] }}</dt><dd>{{ text(field[1]) }}</dd></div>
</dl>
</section>
@@ -389,7 +430,7 @@ onMounted(() => { if (props.detailPage) void loadDetail(); else if (!props.creat
</section>
</section>
<el-card v-else shadow="never" class="form-card looseboard-form-card">
<el-card v-else-if="kind === 'looseboard'" shadow="never" class="form-card looseboard-form-card">
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
<div class="form-section transport-section">
<h3><span class="section-icon"><Truck :size="14" /></span>运输方式 <em>*</em></h3>
@@ -424,6 +465,41 @@ onMounted(() => { if (props.detailPage) void loadDetail(); else if (!props.creat
<div class="form-actions"><el-button @click="returnToList">取消</el-button><el-button v-if="canCreate" type="primary" :loading="submitting" @click="submit">提交订单</el-button></div>
</el-form>
</el-card>
<el-card v-else shadow="never" class="form-card">
<el-form ref="formRef" :model="form" :rules="rules" :label-width="kind === 'trucking' ? '140px' : '120px'">
<el-form-item v-if="kind === 'looseboard'" label="运输方式"><el-radio-group v-model="form.transportMode"><el-radio-button value="SHARED_TRUCK">拼车</el-radio-button><el-radio-button value="PRIORITY_CONSOLIDATION">优先拼载</el-radio-button></el-radio-group></el-form-item>
<el-form-item v-if="kind === 'looseboard'" label="是否打盘"><el-radio-group v-model="form.palletRequired"><el-radio-button value="NO">否</el-radio-button><el-radio-button value="YES">是</el-radio-button></el-radio-group></el-form-item>
<el-form-item v-if="kind === 'looseboard'" label="运输类型"><el-select v-model="form.transportType"><el-option label="港口提柜" value="PORT_PICKUP" /><el-option label="仓到仓" value="WAREHOUSE_TO_WAREHOUSE" /><el-option label="仓到门" value="WAREHOUSE_TO_DOOR" /></el-select></el-form-item>
<el-form-item v-if="kind === 'trucking'" label="车辆类型" prop="vehicleType"><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>
<el-form-item label="起运地" prop="origin"><el-input v-model.trim="form.origin" maxlength="255" placeholder="请输入起运地" /></el-form-item>
<el-form-item label="起运地址"><el-input v-model.trim="form.originAddress" maxlength="500" placeholder="请输入详细起运地址" /></el-form-item>
<template v-if="kind === 'trucking'">
<el-form-item label="起运地联系人" prop="originContactName"><el-input v-model.trim="form.originContactName" maxlength="128" placeholder="请输入起运地联系人" /></el-form-item>
<el-form-item label="起运地联系电话" prop="originContactPhone"><el-input v-model.trim="form.originContactPhone" type="tel" maxlength="128" placeholder="请输入起运地联系电话" /></el-form-item>
</template>
<el-form-item label="目的地" prop="destination"><el-input v-model.trim="form.destination" maxlength="255" placeholder="请输入目的地" /></el-form-item>
<el-form-item label="目的地址"><el-input v-model.trim="form.destinationAddress" maxlength="500" placeholder="请输入详细目的地址" /></el-form-item>
<template v-if="kind === 'trucking'">
<el-form-item label="目的地联系人" prop="destinationContactName"><el-input v-model.trim="form.destinationContactName" maxlength="128" placeholder="请输入目的地联系人" /></el-form-item>
<el-form-item label="目的地联系电话" prop="destinationContactPhone"><el-input v-model.trim="form.destinationContactPhone" type="tel" maxlength="128" placeholder="请输入目的地联系电话" /></el-form-item>
</template>
<el-form-item v-if="kind === 'looseboard'" 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="件数"><el-input-number v-model="form.packageCount" :min="0" placeholder="请输入件数" /></el-form-item>
<el-form-item label="总重量(kg)"><el-input-number v-model="form.totalWeightKg" :min="0" :precision="2" placeholder="请输入总重量" /></el-form-item>
<el-form-item label="总体积(m³)"><el-input-number v-model="form.totalVolumeCbm" :min="0" :precision="3" placeholder="请输入总体积" /></el-form-item>
<el-form-item v-if="kind === 'looseboard'" label="要求时间"><el-date-picker v-model="form.requestedDate" type="date" value-format="YYYY-MM-DD" /></el-form-item>
<template v-if="kind === 'trucking'">
<el-form-item label="取货时间" prop="pickupAt"><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"><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>
</template>
<el-form-item class="budget-form-item" label="预算金额"><el-input-number v-model="form.budgetAmount" :min="0" :precision="2" placeholder="请输入预算金额" /><el-select v-model="form.currency" class="currency"><el-option label="USD" value="USD" /><el-option label="CNY" value="CNY" /></el-select></el-form-item>
<el-form-item v-if="kind === 'looseboard'" label="联系人" prop="contactName"><el-input v-model.trim="form.contactName" maxlength="128" placeholder="请输入联系人" /></el-form-item>
<el-form-item v-if="kind === 'looseboard'" label="联系方式" prop="contactInfo"><el-input v-model.trim="form.contactInfo" maxlength="128" placeholder="请输入电话、微信或邮箱" /></el-form-item>
<el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="4" maxlength="1000" show-word-limit placeholder="请输入其他要求" /></el-form-item>
<el-form-item><el-button @click="returnToList">取消</el-button><el-button v-if="canCreate" type="primary" :loading="submitting" @click="submit">提交订单</el-button></el-form-item>
</el-form>
</el-card>
</section>
</template>
@@ -448,14 +524,16 @@ onMounted(() => { if (props.detailPage) void loadDetail(); else if (!props.creat
.business-detail-section h3 { margin: 0; padding: 0 0 5px; border-bottom: 1px solid #eaf0f7; color: var(--navy); font-size: 13px; }
.detail-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin: 0; border-top: 1px solid var(--border); border-left: 1px solid var(--border); }
.detail-grid div { display: grid; min-width: 0; min-height: 34px; grid-template-columns: 76px minmax(0, 1fr); align-items: center; gap: 7px; padding: 3px 8px; border-right: 1px solid var(--border); border-bottom: 1px solid var(--border); }
.trucking-detail-grid div { grid-template-columns: 112px minmax(0, 1fr); }
.detail-grid div.is-wide { grid-column: span 2; }
.detail-grid dt { color: var(--muted); font-size: 10px; white-space: nowrap; }
.arrangement-grid div { grid-template-columns: 124px minmax(0, 1fr); }
.arrangement-grid dt { white-space: normal; overflow-wrap: anywhere; }
.detail-grid dd { min-width: 0; margin: 0; overflow-wrap: anywhere; color: var(--navy); font-size: 12px; font-weight: 600; line-height: 1.4; }
.form-card :deep(.el-form) { max-width: 860px; }
.form-card :deep(.el-input), .form-card :deep(.el-textarea), .form-card :deep(.el-select) { width: 100%; }
.form-card :deep(.el-input), .form-card :deep(.el-textarea), .form-card :deep(.el-select), .form-card :deep(.el-date-editor) { width: 100%; }
.form-card .currency { width: 110px; margin-left: 12px; }
.warehouse-load-error { color: #b42318; font-size: 12px; }
.looseboard-form-card { padding: 0; border: 1px solid #e7ebf1; border-radius: 10px; background: #fff; box-shadow: 0 8px 22px rgb(31 45 61 / 4%); }
.looseboard-form-card :deep(.el-card__body) { padding: 0; }