feat(looseboard): simplify client demand and split route contacts

This commit is contained in:
2026-10-19 15:11:15 +08:00
parent 04a06668a1
commit 34650b06df
5 changed files with 106 additions and 121 deletions
+11 -1
View File
@@ -8,4 +8,14 @@
| 美国本土卡 | 列表 | 现有 `BusinessOrdersView.vue` 的独立列表与左侧操作列 | 对应列展示车型和取送时间 | 待人工截图确认 | | 美国本土卡 | 列表 | 现有 `BusinessOrdersView.vue` 的独立列表与左侧操作列 | 对应列展示车型和取送时间 | 待人工截图确认 |
| 美国本土卡 | 详情 | 现有独立详情路由及键值网格 | 展示保存的两端联系信息、车型和取送时间;加宽长字段标签 | 待人工截图确认 | | 美国本土卡 | 详情 | 现有独立详情路由及键值网格 | 展示保存的两端联系信息、车型和取送时间;加宽长字段标签 | 待人工截图确认 |
不改动散板字段和承载方式。无新增审核、删除或附件操作。 本节记录本土卡车调整;散板最新范围见下节。无新增审核、删除或附件操作。
## 本土散板简化需求合同
当前字段规则见 `docs/海外仓业务/README.md`“本土散板简化下单”;依据用户提供的 Admin 简化原型及本轮同步消费者确认,不改变客户认证范围。
| 页面 | 操作 | 承载与范围 | 验收 |
|---|---|---|---|
| 美本土散板 | 新增 | 保持现有独立路由和表单容器;同步商品、预计托数、四项联系信息及必填的当地取送时间;删除运输方式、柜型和下单安排项 | 自动测试与人工视觉分开;待人工截图确认 |
| 美本土散板 | 列表、详情 | 保持现有独立路由;展示同一保存值和真实承运安排,不访问运力池 | 待人工截图确认 |
+10 -13
View File
@@ -17,7 +17,6 @@ export interface BusinessOrderQuery {
status?: string status?: string
routeKeyword?: string routeKeyword?: string
transportType?: string transportType?: string
transportMode?: string
vehicleType?: 'PALLET_15' | 'PALLET_30' vehicleType?: 'PALLET_15' | 'PALLET_30'
pickupFrom?: string pickupFrom?: string
pickupTo?: string pickupTo?: string
@@ -31,20 +30,17 @@ export interface TransportSummary {
fleetArrangement: import('../fleetArrangement').FleetArrangement | null fleetArrangement: import('../fleetArrangement').FleetArrangement | null
orderNo: string orderNo: string
transportType?: string transportType?: string
transportMode?: string
palletRequired?: string | null palletRequired?: string | null
origin: string origin: string
destination: string destination: string
containerType?: string | null
palletCount?: number palletCount?: number
packageCount: number packageCount: number
totalWeightKg: number totalWeightKg: number
totalVolumeCbm: number totalVolumeCbm: number
budgetAmount: number | null budgetAmount: number | null
currency: string | null currency: string | null
contactName?: string | null productName?: string | null
contactInfo?: string | null estimatedPalletCount?: number | null
requestedDate?: string | null
vehicleType?: 'PALLET_15' | 'PALLET_30' | null vehicleType?: 'PALLET_15' | 'PALLET_30' | null
originContactName?: string | null originContactName?: string | null
originContactPhone?: string | null originContactPhone?: string | null
@@ -66,22 +62,23 @@ export interface TransportDetail {
} }
export interface TransportCreate { export interface TransportCreate {
transportType: string productName?: string
transportMode?: string estimatedPalletCount: number
palletRequired?: string
origin: string origin: string
originAddress?: string originAddress?: string
destination: string destination: string
destinationAddress?: string destinationAddress?: string
containerType?: string
packageCount: number packageCount: number
totalWeightKg: number totalWeightKg: number
totalVolumeCbm: number totalVolumeCbm: number
budgetAmount?: number budgetAmount?: number
currency?: string currency?: string
contactName?: string originContactName: string
contactInfo?: string originContactPhone: string
requestedDate?: string destinationContactName: string
destinationContactPhone: string
pickupAt: string
deliveryAt: string
remark?: string remark?: string
idempotencyKey: string idempotencyKey: string
} }
@@ -142,7 +142,8 @@ test('truck requires both contacts vehicle and minute times without inheriting l
const loose = transportHarness('looseboard') const loose = transportHarness('looseboard')
assert.equal(loose.rules.value.contactName, undefined) assert.equal(loose.rules.value.contactName, undefined)
assert.equal(loose.rules.value.contactInfo, undefined) assert.equal(loose.rules.value.contactInfo, undefined)
assert.equal(loose.rules.value.transportMode[0].required, true) assert.equal(loose.rules.value.transportMode, undefined)
for (const key of ['originContactName', 'originContactPhone', 'destinationContactName', 'destinationContactPhone', 'pickupAt', 'deliveryAt']) assert.equal(loose.rules.value[key][0].required, true)
truck.form.pickupAt = '2026-10-20T09:30:00' truck.form.pickupAt = '2026-10-20T09:30:00'
let failure let failure
truck.rules.value.deliveryAt[1].validator({}, '2026-10-20T09:29:00', error => { failure = error }) truck.rules.value.deliveryAt[1].validator({}, '2026-10-20T09:29:00', error => { failure = error })
@@ -177,3 +178,30 @@ test('truck details render saved minute times and unknown pool demand without ti
assert.equal(fields.has('柜型'), false) assert.equal(fields.has('柜型'), false)
assert.equal(fields.has('托盘数'), false) assert.equal(fields.has('托盘数'), false)
}) })
test('looseboard saves split contacts and both local minute times without obsolete fields', async () => {
const current = transportHarness('looseboard', true)
Object.assign(current.form, { originContactName: 'A', originContactPhone: '1', destinationContactName: 'B', destinationContactPhone: '2', productName: '家居用品', estimatedPalletCount: 3, packageCount: 8, origin: 'LA', destination: 'ONT', pickupAt: '2026-10-20T09:00:00', deliveryAt: '2026-10-20T15:30:00' })
await current.submit(); await current.submit()
const payload = current.calls[1].payload
assert.equal(payload.estimatedPalletCount, 3)
assert.equal(payload.packageCount, 8)
assert.equal(payload.productName, '家居用品')
assert.equal(payload.pickupAt, '2026-10-20T09:00:00')
assert.equal(payload.originContactName, 'A')
assert.equal(payload.destinationContactPhone, '2')
assert.equal(payload.deliveryAt, '2026-10-20T15:30:00')
assert.equal(payload.idempotencyKey, current.calls[0].payload.idempotencyKey)
for (const field of ['palletRequired', 'transportType', 'transportMode', 'containerType', 'contactName', 'contactInfo', 'requestedDate']) assert.equal(Object.hasOwn(payload, field), false)
})
test('looseboard requires both times and rejects backwards schedule', () => {
const current = transportHarness('looseboard')
assert.equal(current.rules.value.pickupAt[0].required, true)
assert.equal(current.rules.value.deliveryAt[0].required, true)
let failure
current.form.pickupAt = '2026-10-20T10:30:00'
current.rules.value.deliveryAt[1].validator({}, '2026-10-20T10:29:00', error => { failure = error })
assert.ok(failure)
})
@@ -1,13 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from 'vue' import { computed, onMounted, reactive, ref, watch } from 'vue'
import { Package, Truck } from '@lucide/vue' import { Package } from '@lucide/vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus' import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue' import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue' import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
import type { TableAction } from '@/components/common/tableActions' import type { TableAction } from '@/components/common/tableActions'
import { useAuthStore } from '@/modules/auth/public' import { useAuthStore } from '@/modules/auth/public'
import { getMerchantWarehouses, type MerchantWarehouse } from '@/modules/first-leg/public'
import { import {
createTransport, createTransport,
getBusinessOrder, getBusinessOrder,
@@ -16,7 +15,6 @@ import {
type TransportDetail, type TransportDetail,
type TransportSummary, type TransportSummary,
} from '../api/businessOrders' } from '../api/businessOrders'
import { CONTAINER_SPECIFICATIONS, DEFAULT_CONTAINER_TYPE } from '../containerSpecifications'
import { poolOrderStatusLabels } from '../fleetArrangement' import { poolOrderStatusLabels } from '../fleetArrangement'
const props = withDefaults(defineProps<{ kind: CrossBorderBusinessKind; createPage?: boolean; detailPage?: boolean }>(), { createPage: false, detailPage: false }) const props = withDefaults(defineProps<{ kind: CrossBorderBusinessKind; createPage?: boolean; detailPage?: boolean }>(), { createPage: false, detailPage: false })
@@ -36,14 +34,12 @@ type PageMode = 'list' | 'create' | 'detail'
interface ColumnDefinition { key: string; label: string; width?: number } interface ColumnDefinition { key: string; label: string; width?: number }
interface BusinessForm { interface BusinessForm {
transportMode: string productName: string
palletRequired: string estimatedPalletCount: number
transportType: string
origin: string origin: string
originAddress: string originAddress: string
destination: string destination: string
destinationAddress: string destinationAddress: string
containerType: string
packageCount: number packageCount: number
totalWeightKg: number totalWeightKg: number
totalVolumeCbm: number totalVolumeCbm: number
@@ -56,13 +52,7 @@ interface BusinessForm {
destinationContactPhone: string destinationContactPhone: string
pickupAt: string pickupAt: string
deliveryAt: string deliveryAt: string
contactName: string
contactInfo: string
requestedDate: string
remark: string remark: string
cargoName: string
deliveryDate: string
palletCount: number
} }
const configuration = computed(() => ({ const configuration = computed(() => ({
@@ -81,14 +71,13 @@ const configuration = computed(() => ({
function transportColumns(looseboard: boolean): ColumnDefinition[] { function transportColumns(looseboard: boolean): ColumnDefinition[] {
return [ return [
{ key: 'orderNo', label: '订单编号', width: 180 }, { key: 'orderNo', label: '订单编号', width: 180 },
...(looseboard ? [{ key: 'transportMode', label: '运输方式', width: 120 }, { key: 'palletRequired', label: '是否打盘', width: 100 }] : []), ...(!looseboard ? [{ key: 'vehicleType', label: '车辆类型', width: 110 }] : []),
...(looseboard ? [{ key: 'transportType', label: '运输类型', width: 130 }] : [{ key: 'vehicleType', label: '车辆类型', width: 110 }]),
{ key: 'origin', label: '起运地', width: 150 }, { key: 'origin', label: '起运地', width: 150 },
{ key: 'destination', label: '目的地', width: 150 }, { key: 'destination', label: '目的地', width: 150 },
...(looseboard ? [{ key: 'containerType', label: '柜型', width: 100 }] : []), ...(looseboard ? [{ key: 'productName', label: '商品名称', width: 150 }, { key: 'estimatedPalletCount', label: '预计托数', width: 100 }] : []),
{ key: 'totalWeightKg', label: '总重量(kg)', width: 120 }, { key: 'totalWeightKg', label: '总重量(kg)', width: 120 },
{ key: 'budgetAmount', label: '预算总价', width: 110 }, { key: 'budgetAmount', label: '预算总价', width: 110 },
...(looseboard ? [{ key: 'requestedDate', label: '要求时间', width: 120 }] : [ ...([
{ key: 'pickupAt', label: '取货时间', width: 165 }, { key: 'deliveryAt', label: '送货时间', width: 165 }, { key: 'pickupAt', label: '取货时间', width: 165 }, { key: 'deliveryAt', label: '送货时间', width: 165 },
]), ]),
{ key: 'status', label: '状态', width: 130 }, { key: 'status', label: '状态', width: 130 },
@@ -107,10 +96,6 @@ const fleetArrangement = computed(() => detail.value ? (detail.value as Transpor
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>()
const query = reactive({ page: 1, size: 20, orderNo: '', routeKeyword: '', status: '' }) const query = reactive({ page: 1, size: 20, orderNo: '', routeKeyword: '', status: '' })
const total = ref(0) const total = ref(0)
const overseasWarehouses = ref<MerchantWarehouse[]>([])
const overseasWarehousesLoading = ref(false)
const overseasWarehousesError = ref('')
const isSelfDelivery = computed(() => props.kind === 'looseboard' && form.transportMode === 'SHARED_TRUCK')
const queryModel = computed<QueryModel>({ const queryModel = computed<QueryModel>({
get: () => ({ get: () => ({
@@ -151,22 +136,15 @@ const form = reactive<BusinessForm>(emptyForm())
const rules = computed<FormRules<BusinessForm>>(() => ({ const rules = computed<FormRules<BusinessForm>>(() => ({
origin: [{ required: true, message: '请输入起运地', trigger: 'blur' }], origin: [{ required: true, message: '请输入起运地', trigger: 'blur' }],
destination: [{ required: true, message: '请输入目的地', trigger: 'blur' }], destination: [{ required: true, message: '请输入目的地', trigger: 'blur' }],
...(props.kind === 'trucking' ? { ...(props.kind === 'trucking' ? { vehicleType: [{ required: true, message: '请选择车辆类型', trigger: 'change' }] } : {}),
vehicleType: [{ required: true, message: '请选择车辆类型', trigger: 'change' }], originContactName: [{ required: true, message: '请输入起运地联系人', trigger: 'blur' }],
originContactName: [{ required: true, message: '请输入起运地联系人', trigger: 'blur' }], originContactPhone: [{ required: true, message: '请输入起运地联系电话', trigger: 'blur' }],
originContactPhone: [{ required: true, message: '请输入起运地联系电话', trigger: 'blur' }], destinationContactName: [{ required: true, message: '请输入目的地联系人', trigger: 'blur' }],
destinationContactName: [{ required: true, message: '请输入目的地联系人', trigger: 'blur' }], destinationContactPhone: [{ required: true, message: '请输入目的地联系电话', trigger: 'blur' }],
destinationContactPhone: [{ required: true, message: '请输入目的地联系电话', trigger: 'blur' }], pickupAt: [{ required: true, message: '请选择取货时间', trigger: 'change' }],
pickupAt: [{ required: true, message: '请选择取货时间', trigger: 'change' }], deliveryAt: [{ 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',
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(() => { const detailFields = computed(() => {
@@ -175,21 +153,15 @@ const detailFields = computed(() => {
const current = detail.value as TransportDetail const current = detail.value as TransportDetail
return [ return [
...(props.kind === 'looseboard' ? [ ...(props.kind === 'looseboard' ? [
['运输方式', transportModeLabel(read(summary, 'transportMode'))], ['是否打盘', yesNo(read(summary, 'palletRequired'))], ['商品名称', summary.productName], ['预计托数', summary.estimatedPalletCount],
['运输类型', transportTypeLabel(read(summary, 'transportType'))],
] : [['车辆类型', vehicleTypeLabel(summary.vehicleType)]]), ] : [['车辆类型', vehicleTypeLabel(summary.vehicleType)]]),
['起运地', read(summary, 'origin')], ['起运地址', current.originAddress], ['起运地', read(summary, 'origin')], ['起运地址', current.originAddress],
['目的地', read(summary, 'destination')], ['目的地址', current.destinationAddress], ['目的地', read(summary, 'destination')], ['目的地址', current.destinationAddress],
...(props.kind === 'looseboard' ? [['柜型', read(summary, 'containerType')]] : []),
['件数', read(summary, 'packageCount')], ['总重量(kg)', read(summary, 'totalWeightKg')], ['件数', read(summary, 'packageCount')], ['总重量(kg)', read(summary, 'totalWeightKg')],
['总体积(m³)', read(summary, 'totalVolumeCbm')], ['预算总价', money(read(summary, 'budgetAmount'), read(summary, 'currency'))], ['总体积(m³)', read(summary, 'totalVolumeCbm')], ['预算总价', money(read(summary, 'budgetAmount'), read(summary, 'currency'))],
...(props.kind === 'looseboard' ? [ ['起运地联系人', summary.originContactName], ['起运地联系电话', summary.originContactPhone],
['联系人', summary.contactName], ['联系方式', summary.contactInfo], ['要求时间', summary.requestedDate], ['目的地联系人', summary.destinationContactName], ['目的地联系电话', summary.destinationContactPhone],
] : [ ['取货时间', localMinute(summary.pickupAt)], ['送货时间', localMinute(summary.deliveryAt)], ['备注', current.remark],
['起运地联系人', summary.originContactName], ['起运地联系电话', summary.originContactPhone],
['目的地联系人', summary.destinationContactName], ['目的地联系电话', summary.destinationContactPhone],
['取货时间', localMinute(summary.pickupAt)], ['送货时间', localMinute(summary.deliveryAt)],
]), ['备注', current.remark],
] ]
}) })
@@ -207,33 +179,14 @@ const detailStatus = computed(() => detail.value ? statusLabel(detail.value.summ
function emptyForm(): BusinessForm { function emptyForm(): BusinessForm {
return { return {
transportMode: 'SHARED_TRUCK', palletRequired: 'NO', transportType: 'PORT_PICKUP', origin: '', originAddress: '', productName: '', estimatedPalletCount: 0, origin: '', originAddress: '',
destination: '', destinationAddress: '', containerType: DEFAULT_CONTAINER_TYPE, packageCount: 0, destination: '', destinationAddress: '', packageCount: 0,
totalWeightKg: 0, totalVolumeCbm: 0, currency: 'USD', contactName: '', contactInfo: '', requestedDate: '', totalWeightKg: 0, totalVolumeCbm: 0, currency: 'USD',
remark: '', cargoName: '', deliveryDate: '', palletCount: 0, vehicleType: '', originContactName: '', originContactPhone: '', destinationContactName: '', destinationContactPhone: '', pickupAt: '', deliveryAt: '', remark: '', vehicleType: '', originContactName: '', originContactPhone: '', destinationContactName: '', destinationContactPhone: '', pickupAt: '', deliveryAt: '',
} }
} }
function resetForm() { Object.assign(form, emptyForm()); submitKey.value = ''; formRef.value?.clearValidate() } function resetForm() { Object.assign(form, emptyForm()); submitKey.value = ''; formRef.value?.clearValidate() }
function warehouseLabel(warehouse: MerchantWarehouse) { return `${warehouse.warehouseName} · ${warehouse.warehouseCode}` }
function selectOriginMode() {
form.origin = ''
form.originAddress = ''
formRef.value?.clearValidate('origin')
}
async function loadOverseasWarehouses() {
if (props.kind !== 'looseboard') return
overseasWarehousesLoading.value = true
overseasWarehousesError.value = ''
try {
const result = await getMerchantWarehouses({ page: 1, size: 200, warehouseType: 'OVERSEAS' })
overseasWarehouses.value = result.records.filter(item => item.status === 'ACTIVE' && ['OVERSEAS', 'OVERSEAS_PARTNER'].includes(item.warehouseType) && item.countryCode === 'US')
if (!overseasWarehouses.value.length) overseasWarehousesError.value = '暂无可用的海外仓'
} catch (cause) {
overseasWarehouses.value = []
overseasWarehousesError.value = errorMessage(cause)
} finally { overseasWarehousesLoading.value = false }
}
function read(source: object, key: string) { return Reflect.get(source, key) as unknown } function read(source: object, key: string) { return Reflect.get(source, key) as unknown }
function text(value: unknown) { return value === null || value === undefined || value === '' ? '—' : String(value) } function text(value: unknown) { return value === null || value === undefined || value === '' ? '—' : String(value) }
function money(amount: unknown, currency: unknown) { return amount === null || amount === undefined ? '—' : `${text(currency)} ${Number(amount).toFixed(2)}` } function money(amount: unknown, currency: unknown) { return amount === null || amount === undefined ? '—' : `${text(currency)} ${Number(amount).toFixed(2)}` }
@@ -242,7 +195,6 @@ function yesNo(value: unknown) { return value === 'YES' ? '是' : value === 'NO'
function vehicleTypeLabel(value: unknown) { return ({ PALLET_15: '15拖', PALLET_30: '30拖' } as Record<string, string>)[String(value)] || 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 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 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) { function statusLabel(value: unknown) {
if (poolOrderStatusLabels[String(value)]) return poolOrderStatusLabels[String(value)] if (poolOrderStatusLabels[String(value)]) return poolOrderStatusLabels[String(value)]
return ({ PENDING_REVIEW: '待审核', APPROVED: '已通过', REJECTED: '已拒绝', PENDING_ACCEPTANCE: '待接单', PROCESSING: '处理中', IN_TRANSIT: '运输中', COMPLETED: '已完成', CANCELLED: '已取消' } as Record<string, string>)[String(value)] || text(value) return ({ PENDING_REVIEW: '待审核', APPROVED: '已通过', REJECTED: '已拒绝', PENDING_ACCEPTANCE: '待接单', PROCESSING: '处理中', IN_TRANSIT: '运输中', COMPLETED: '已完成', CANCELLED: '已取消' } as Record<string, string>)[String(value)] || text(value)
@@ -257,7 +209,6 @@ function displayCell(candidate: unknown, key: string) {
if (key === 'vehicleType') return vehicleTypeLabel(value) if (key === 'vehicleType') return vehicleTypeLabel(value)
if (key === 'pickupAt' || key === 'deliveryAt') return localMinute(value) if (key === 'pickupAt' || key === 'deliveryAt') return localMinute(value)
if (key === 'transportType') return transportTypeLabel(value) if (key === 'transportType') return transportTypeLabel(value)
if (key === 'transportMode') return transportModeLabel(value)
if (key === 'palletRequired') return yesNo(value) if (key === 'palletRequired') return yesNo(value)
if (key === 'createdAt') return dateTime(value) if (key === 'createdAt') return dateTime(value)
if (key === 'budgetAmount') return money(value, read(row, 'currency')) if (key === 'budgetAmount') return money(value, read(row, 'currency'))
@@ -336,6 +287,7 @@ async function loadDetail() {
} }
async function submit() { async function submit() {
if (props.kind === 'looseboard' && form.pickupAt && form.deliveryAt && form.deliveryAt < form.pickupAt) { ElMessage.error('送达时间不能早于取货时间'); return }
if (!await formRef.value?.validate().catch(() => false)) return if (!await formRef.value?.validate().catch(() => false)) return
submitting.value = true submitting.value = true
const stableSubmitKey = submitKey.value || idempotencyKey() const stableSubmitKey = submitKey.value || idempotencyKey()
@@ -347,7 +299,7 @@ async function submit() {
packageCount: form.packageCount, packageCount: form.packageCount,
totalWeightKg: form.totalWeightKg, totalVolumeCbm: form.totalVolumeCbm, totalWeightKg: form.totalWeightKg, totalVolumeCbm: form.totalVolumeCbm,
budgetAmount: form.budgetAmount, currency: form.currency || undefined, budgetAmount: form.budgetAmount, currency: form.currency || undefined,
remark: props.kind === 'looseboard' ? [form.cargoName, form.remark].filter(Boolean).join(' · ') || undefined : form.remark || undefined, remark: form.remark || undefined,
idempotencyKey: stableSubmitKey, idempotencyKey: stableSubmitKey,
} }
if (props.kind === 'trucking') { if (props.kind === 'trucking') {
@@ -358,9 +310,10 @@ async function submit() {
pickupAt: form.pickupAt, deliveryAt: form.deliveryAt, pickupAt: form.pickupAt, deliveryAt: form.deliveryAt,
}) })
} else { } else {
await createTransport('looseboard', { ...base, transportMode: form.transportMode, palletRequired: form.palletRequired, await createTransport('looseboard', { ...base, productName: form.productName || undefined, estimatedPalletCount: form.estimatedPalletCount,
transportType: form.transportType, containerType: form.containerType || undefined, originContactName: form.originContactName, originContactPhone: form.originContactPhone,
contactName: form.contactName || undefined, contactInfo: form.contactInfo || undefined, requestedDate: form.requestedDate || undefined, destinationContactName: form.destinationContactName, destinationContactPhone: form.destinationContactPhone,
pickupAt: form.pickupAt, deliveryAt: form.deliveryAt,
}) })
} }
ElMessage.success('订单提交成功') ElMessage.success('订单提交成功')
@@ -372,7 +325,7 @@ async function submit() {
} }
watch(() => [query.page, query.size], () => { if (!props.createPage && mode.value === 'list') void load() }) watch(() => [query.page, query.size], () => { if (!props.createPage && mode.value === 'list') void load() })
onMounted(() => { if (props.detailPage) void loadDetail(); else if (!props.createPage) void load(); void loadOverseasWarehouses() }) onMounted(() => { if (props.detailPage) void loadDetail(); else if (!props.createPage) void load() })
</script> </script>
<template> <template>
@@ -432,36 +385,32 @@ onMounted(() => { if (props.detailPage) void loadDetail(); else if (!props.creat
<el-card v-else-if="kind === 'looseboard'" 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"> <el-form ref="formRef" :model="form" :rules="rules" label-position="top">
<div class="form-section transport-section"> <div class="form-grid two-col lower-grid">
<h3><span class="section-icon"><Truck :size="14" /></span>运输方式 <em>*</em></h3> <el-form-item label="起运地" prop="origin"><el-input v-model.trim="form.origin" maxlength="255" placeholder="城市 / 港口 / 州" /></el-form-item>
<div class="mode-buttons"><el-radio-group v-model="form.transportMode" @change="selectOriginMode"><el-radio-button value="SHARED_TRUCK"><Truck :size="14" /> 自送</el-radio-button><el-radio-button value="PRIORITY_CONSOLIDATION"><Package :size="14" /> 我司提货</el-radio-button></el-radio-group></div>
</div>
<div class="form-grid two-col">
<el-form-item :label="isSelfDelivery ? '入仓库' : '取货地'" prop="origin" :rules="[{ required: true, message: isSelfDelivery ? '请选择美国仓库' : '请输入取货地', trigger: isSelfDelivery ? 'change' : 'blur' }]">
<template v-if="isSelfDelivery">
<el-select v-model="form.origin" filterable clearable :loading="overseasWarehousesLoading" placeholder="请选择美国仓库" no-data-text="暂无可用的美国海外仓">
<el-option v-for="warehouse in overseasWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouseLabel(warehouse)" />
</el-select>
<div v-if="overseasWarehousesError" role="alert" class="warehouse-load-error">{{ overseasWarehousesError }} <el-button link type="primary" @click="loadOverseasWarehouses">重新加载</el-button></div>
</template>
<el-input v-else 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="destination"><el-input v-model.trim="form.destination" maxlength="255" placeholder="城市 / 州" /></el-form-item>
<el-form-item label="联系人" prop="contactName"><el-input v-model.trim="form.contactName" maxlength="128" placeholder="联系人姓名" /></el-form-item> <el-form-item label="起运地联系人" prop="originContactName"><el-input v-model.trim="form.originContactName" maxlength="128" placeholder="请输入起运地联系人" /></el-form-item>
<el-form-item label="联系方式" prop="contactInfo"><el-input v-model.trim="form.contactInfo" maxlength="128" placeholder="联系电话" /></el-form-item> <el-form-item label="目的地联系人" prop="destinationContactName"><el-input v-model.trim="form.destinationContactName" 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>
<el-form-item label="目的地联系电话" prop="destinationContactPhone"><el-input v-model.trim="form.destinationContactPhone" type="tel" maxlength="128" placeholder="请输入目的地联系电话" /></el-form-item>
</div> </div>
<div class="form-section cargo-section"><h3><span class="section-icon"><Package :size="14" /></span>托载</h3><div class="form-grid cargo-grid"> <div class="form-section cargo-section"><h3><span class="section-icon"><Package :size="14" /></span>托载</h3><div class="form-grid cargo-grid">
<el-form-item label="商品名称"><el-input v-model.trim="form.cargoName" placeholder="请输入商品名称" /></el-form-item> <el-form-item label="商品名称"><el-input v-model.trim="form.productName" placeholder="请输入商品名称" /></el-form-item>
<el-form-item label="托数"><el-input-number v-model="form.palletCount" :min="0" controls-position="right" /></el-form-item> <el-form-item label="预计托数"><el-input-number v-model="form.estimatedPalletCount" :min="0" controls-position="right" placeholder="请输入预计托数" /></el-form-item>
<el-form-item label="总重量 (kg)"><el-input-number v-model="form.totalWeightKg" :min="0" :precision="2" controls-position="right" /></el-form-item> <el-form-item label="总重量 (kg)"><el-input-number v-model="form.totalWeightKg" :min="0" :precision="2" controls-position="right" placeholder="请输入总重量" /></el-form-item>
<el-form-item label="总体积 (m³)"><el-input-number v-model="form.totalVolumeCbm" :min="0" :precision="3" controls-position="right" /></el-form-item> <el-form-item label="总体积 (m³)"><el-input-number v-model="form.totalVolumeCbm" :min="0" :precision="3" controls-position="right" placeholder="请输入总体积" /></el-form-item>
</div></div> </div></div>
<div class="form-grid two-col lower-grid"> <div class="form-grid two-col lower-grid">
<el-form-item label="预算总价($)"><el-input-number v-model="form.budgetAmount" :min="0" :precision="2" controls-position="right" placeholder="0.00" /></el-form-item> <el-form-item label="预算总价"><el-input-number v-model="form.budgetAmount" :min="0" :precision="2" controls-position="right" placeholder="0.00" /></el-form-item>
<div></div> <el-form-item label="预算币种"><el-select v-model="form.currency"><el-option label="USD" value="USD" /><el-option label="CNY" value="CNY" /></el-select></el-form-item>
<el-form-item label="要求取货时间" prop="requestedDate"><el-date-picker v-model="form.requestedDate" type="date" value-format="YYYY-MM-DD" placeholder="年 / 月 / 日" /></el-form-item> <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="送货到达时间"><el-date-picker v-model="form.deliveryDate" type="date" value-format="YYYY-MM-DD" placeholder="年 / 月 / 日" /></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>
</div> </div>
<el-collapse class="looseboard-extra"><el-collapse-item title="补充信息(选填)" name="extra"><div class="form-grid two-col">
<el-form-item label="件数"><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" maxlength="1000" :rows="2" placeholder="请输入备注(选填)" /></el-form-item>
<el-form-item label="起运地址补充"><el-input v-model.trim="form.originAddress" maxlength="500" placeholder="请输入详细起运地址(选填)" /></el-form-item>
<el-form-item label="目的地址补充"><el-input v-model.trim="form.destinationAddress" maxlength="500" placeholder="请输入详细目的地址(选填)" /></el-form-item>
</div></el-collapse-item></el-collapse>
<div class="form-actions"><el-button @click="returnToList">取消</el-button><el-button v-if="canCreate" type="primary" :loading="submitting" @click="submit">提交订单</el-button></div> <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-form>
</el-card> </el-card>
@@ -551,6 +500,7 @@ onMounted(() => { if (props.detailPage) void loadDetail(); else if (!props.creat
.lower-grid { padding-top: 20px; } .lower-grid { padding-top: 20px; }
.lower-grid :deep(.el-date-editor) { width: 100%; } .lower-grid :deep(.el-date-editor) { width: 100%; }
.form-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 5px; padding: 20px 30px 25px; border-top: 1px solid #f0f2f5; } .form-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 5px; padding: 20px 30px 25px; border-top: 1px solid #f0f2f5; }
.looseboard-extra { margin: 0 30px; }
.looseboard-form-card .form-actions { padding-right: max(30px, calc(100% - 1150px)); } .looseboard-form-card .form-actions { padding-right: max(30px, calc(100% - 1150px)); }
.form-actions .el-button { min-width: 80px; height: 36px; border-radius: 7px; } .form-actions .el-button { min-width: 80px; height: 36px; border-radius: 7px; }
.form-actions .el-button--primary { background: linear-gradient(135deg, #6774df, #7553aa); border: 0; box-shadow: 0 5px 12px rgb(103 116 223 / 24%); } .form-actions .el-button--primary { background: linear-gradient(135deg, #6774df, #7553aa); border: 0; box-shadow: 0 5px 12px rgb(103 116 223 / 24%); }
+5 -5
View File
@@ -52,13 +52,13 @@ test('keeps standalone business pages at the client sidebar root', () => {
{ title: '首页', children: undefined }, { title: '首页', children: undefined },
{ {
title: '订单中心', title: '订单中心',
children: ['头程订单', '入库订单', '派送订单'], children: ['头程订单', '入库订单', '派送订单', '海外采购', '美国本土卡', '美本土散板'],
}, },
{ title: '报关资料', children: undefined }, { title: '报关资料', children: undefined },
{ title: '船期价格', children: undefined }, { title: '船期价格', children: undefined },
{ {
title: '海外仓', title: '海外仓',
children: ['贴标申请', '仓储转运', '货物废弃', '退货换标', '海外采购', '美国本土卡', '美本土散板'], children: ['贴标申请', '仓储转运', '货物废弃', '退货换标'],
}, },
{ {
title: '费用与账单', title: '费用与账单',
@@ -90,15 +90,15 @@ test('flattens visible leaves with unique paths', () => {
'/orders/first-leg', '/orders/first-leg',
'/orders/inbound', '/orders/inbound',
'/orders/delivery', '/orders/delivery',
'/cross-border/procurement',
'/cross-border/domestic-trucking',
'/cross-border/pallet-shipping',
'/first-leg/customs-documents', '/first-leg/customs-documents',
'/first-leg/sailing-prices', '/first-leg/sailing-prices',
'/overseas-warehouse/labeling', '/overseas-warehouse/labeling',
'/overseas-warehouse/storage-transfer', '/overseas-warehouse/storage-transfer',
'/overseas-warehouse/cargo-disposal', '/overseas-warehouse/cargo-disposal',
'/overseas-warehouse/return-relabel', '/overseas-warehouse/return-relabel',
'/cross-border/procurement',
'/cross-border/domestic-trucking',
'/cross-border/pallet-shipping',
'/finance/account', '/finance/account',
'/finance/bills', '/finance/bills',
'/company/profile', '/company/profile',