This commit is contained in:
2026-08-04 12:01:05 +08:00
parent f533024501
commit 3877af0b72
9 changed files with 182 additions and 181 deletions
+1 -1
View File
@@ -45,6 +45,6 @@ pnpm test
企业资料和资质列表使用真实租户接口,覆盖 `PROFILE_REQUIRED`、`PENDING`、`ACTIVE`、`REJECTED`、`DISABLED` 五种企业状态。企业名称、法定名称和联系人信息支持真实保存;营业执照、法人身份证、货代资质证书和银行开户许可证使用文件服务分别上传,接受 PDF、JPG、PNG 且不超过 5MB,四类齐全后显式提交审核,文件预览通过当前商户 Token 临时下载并在关闭弹窗后释放。
头程订单列表、详情、仓库查询、创建/编辑草稿和提交已经接入真实 API,并显式映射后端 `transportMode`、`containerMode` 和订单状态。创建与提交请求携带幂等键,`merchant:order:create`、`merchant:order:edit`、`merchant:order:submit` 分别映射到独立前端操作权限。页面补齐渠道、提货方式、报关模式、目的仓类型、包装类型等后端必填字段;API 模式先清空本地记录,请求失败不会回退模拟成功。收发货单位通过 `/merchant/v1/partners/candidates` 一次读取当前租户启用候选;后端主数据类型 `CUSTOMER / SUPPLIER / BOTH` 不等同于订单角色,同一候选可用于发货方或收货方并分别保存快照。订单写请求以发货方 ID 填充后端强制校验的主往来单位 `customerId`。快递信息仍由平台维护、客户只读。订单取消不属于本轮本地一期验收,客户端不暴露取消写操作。订单产品状态仍按已确认的七状态执行,不设置“已驳回”;若兼容旧接口数据收到 `REJECTED`,只按待审核展示且不开放编辑。
头程订单列表、详情、仓库查询、创建/编辑草稿和提交已经接入真实 API,并显式映射后端 `transportMode`、`containerMode` 和订单状态。创建与提交请求携带幂等键,`merchant:order:create`、`merchant:order:edit`、`merchant:order:submit` 分别映射到独立前端操作权限。页面补齐渠道、提货方式、报关模式、目的仓类型、包装类型等后端必填字段;API 模式先清空本地记录,请求失败不会回退模拟成功。FBA 和海外仓按公共仓类型筛选,选择后由仓库主数据生成目的仓字段,不再要求客户重复选择收货单位;私人仓才填写并按当前用户缓存收件资料。只有“我司提货”需要填写提货联系人和地址,其他送货方式不发送发货方快照,订单也不再依赖往来单位 `customerId`。快递信息仍由平台维护、客户只读。订单取消不属于本轮本地一期验收,客户端不暴露取消写操作。订单产品状态仍按已确认的七状态执行,不设置“已驳回”;若兼容旧接口数据收到 `REJECTED`,只按待审核展示且不开放编辑。
联调远端 Gateway 时可在启动命令前设置 `API_PROXY_TARGET=http://8.163.30.128:9000`;进程环境变量优先于 `.env` 文件。浏览器仍统一请求同源 `/api`,源码中不写死业务服务地址。联调时可只读检查同工作区的 `baiyue-firstleg/` Controller、DTO、Service、Repository 和回归脚本,用于发现后端拉取后的契约变化,但不修改后端仓库。2026-07-31 测试 Gateway 登录和真实角色列表读取正常;Warehouse 服务已注册,但仓库和往来单位业务查询仍返回 `S0500`,头程草稿创建和相关浏览器流程需待后端修复后继续验收。
+2 -2
View File
@@ -95,8 +95,8 @@ export interface FirstLegOrderSaveRequest {
destinationWarehouseName: string
expectedShipDate?: string
customerRemark?: string
sender: FirstLegOrderPartyPayload
receiver: FirstLegOrderPartyPayload
sender?: FirstLegOrderPartyPayload
receiver?: FirstLegOrderPartyPayload
cargoes: FirstLegOrderCargoPayload[]
pickupMethod: ApiPickupMethod
receivingWarehouseId: number | string
@@ -6,7 +6,7 @@ const source = readFileSync(new URL('./firstLegPartySource.ts', import.meta.url)
const viewSource = readFileSync(new URL('../views/FirstLegOrdersView.vue', import.meta.url), 'utf8')
const apiSource = readFileSync(new URL('../api/partners.ts', import.meta.url), 'utf8')
test('keeps counterparty availability behind a replaceable read-only source', () => {
test('keeps the legacy counterparty source available without coupling it to first-leg ordering', () => {
assert.match(source, /export interface FirstLegPartySource/)
assert.match(source, /listAvailable\(role/)
assert.match(source, /findAvailable\(id, role/)
@@ -18,10 +18,8 @@ test('keeps counterparty availability behind a replaceable read-only source', ()
assert.match(source, /: \['SHIPPER', 'CONSIGNEE'\]/)
assert.match(source, /await getMerchantPartnerCandidates\(\)/)
assert.doesNotMatch(source, /getMerchantPartnerCandidates\('SENDER'\)|getMerchantPartnerCandidates\('RECEIVER'\)/)
assert.match(viewSource, /firstLegPartySource\.listAvailable\('SHIPPER'\)/)
assert.match(viewSource, /firstLegPartySource\.listAvailable\('CONSIGNEE'\)/)
assert.match(viewSource, /firstLegPartySource\.findAvailable\(id, role\)/)
assert.match(viewSource, /firstLegPartySource\.load\(\)/)
assert.doesNotMatch(viewSource, /firstLegPartySource/)
assert.doesNotMatch(viewSource, /发货单位|收货单位/)
assert.doesNotMatch(viewSource, /DEMO_FIRST_LEG_PARTIES/)
assert.match(apiSource, /url: '\/merchant\/v1\/partners\/candidates'/)
assert.match(apiSource, /'CARRIER' \| 'CUSTOMS_BROKER' \| 'SHIPPER' \| 'CONSIGNEE'/)
@@ -33,9 +33,11 @@ test('sends backend-required channel, customs, destination and cargo fields', ()
assert.match(storeSource, new RegExp(field))
}
assert.match(storeSource, /containerMode: draft\.transportMode === 'FCL' \? 'FULL_CONTAINER'/)
assert.match(storeSource, /customerId: draft\.shipperPartyId/)
assert.match(storeSource, /customerName: customerName\.trim\(\) \|\| draft\.shipperPartySnapshot\.name/)
assert.doesNotMatch(storeSource, /customerId: draft\.shipperPartyId/)
assert.match(storeSource, /customerName: customerName\.trim\(\)/)
assert.match(storeSource, /destinationWarehouseId: draft\.destinationType === 'PRIVATE' \? undefined : draft\.destinationWarehouseId/)
assert.match(storeSource, /draft\.pickupMethod === 'PICKUP'.*draft\.shipperPartySnapshot/s)
assert.match(storeSource, /draft\.destinationType === 'PRIVATE'.*draft\.consigneePartySnapshot/s)
})
test('separates domestic receiving warehouses from public destinations', () => {
+19 -9
View File
@@ -67,12 +67,22 @@ function partyPayload(snapshot: FirstLegPartySnapshot): FirstLegOrderPartyPayloa
}
function savePayload(draft: FirstLegOrderDraft, warehouse: ClientWarehouse, customerName: string): FirstLegOrderSaveRequest {
if (!draft.shipperPartySnapshot || !draft.consigneePartySnapshot) throw new Error('请完整填写收发货信息')
const sender = draft.pickupMethod === 'PICKUP' && draft.shipperPartySnapshot
? partyPayload({
...draft.shipperPartySnapshot,
name: draft.shipperPartySnapshot.name || customerName.trim() || '客户',
countryRegion: draft.shipperPartySnapshot.countryRegion || 'CN',
})
: undefined
const receiver = draft.destinationType === 'PRIVATE' && draft.consigneePartySnapshot
? partyPayload({
...draft.consigneePartySnapshot,
name: draft.destinationWarehouseName,
countryRegion: draft.destinationCountryCode.trim().toUpperCase(),
})
: undefined
return {
// 后端把 customerId 作为订单主往来单位校验并以服务端名称覆盖 customerName;
// 当前商户下单以所选发货单位作为主往来单位,收发货详情仍分别保存订单快照。
customerId: draft.shipperPartyId,
customerName: customerName.trim() || draft.shipperPartySnapshot.name,
customerName: customerName.trim(),
transportMode: apiTransport(draft),
tradeTerm: draft.tradeTerm || undefined,
destinationCountryCode: draft.destinationCountryCode.trim().toUpperCase(),
@@ -80,8 +90,8 @@ function savePayload(draft: FirstLegOrderDraft, warehouse: ClientWarehouse, cust
destinationWarehouseName: draft.destinationWarehouseName,
expectedShipDate: draft.expectedDepartureDate || undefined,
customerRemark: draft.customerRemark || undefined,
sender: partyPayload(draft.shipperPartySnapshot),
receiver: partyPayload(draft.consigneePartySnapshot),
sender,
receiver,
cargoes: draft.items.map((item) => ({
cargoName: item.cargoNameCn,
cargoNameEn: item.cargoNameEn || undefined,
@@ -107,8 +117,8 @@ function savePayload(draft: FirstLegOrderDraft, warehouse: ClientWarehouse, cust
containerMode: draft.transportMode === 'FCL' ? 'FULL_CONTAINER' : draft.transportMode === 'LCL' ? 'LCL' : undefined,
customsInspection: draft.customsInspection,
destinationType: draft.destinationType,
amazonWarehouseCode: draft.amazonWarehouseCode || undefined,
shipmentId: draft.shipmentId || undefined,
amazonWarehouseCode: draft.destinationType === 'FBA' ? draft.amazonWarehouseCode || undefined : undefined,
shipmentId: draft.destinationType === 'FBA' ? draft.shipmentId || undefined : undefined,
referenceId: draft.referenceNo || undefined,
}
}
@@ -74,28 +74,36 @@ test('validates the confirmed minimum submission fields', () => {
assert.deepEqual(validateFirstLegOrderDraft(validDraft()), [])
const invalid = validDraft()
invalid.origin = ''
invalid.receivingWarehouseId = ''
invalid.items[0].grossWeightKg = 0
invalid.items[0].containerType = ''
invalid.items[0].containerCount = 0
const errors = validateFirstLegOrderDraft(invalid)
assert.ok(errors.includes('请填写起运地'))
assert.ok(errors.includes('请选择平台收货仓'))
assert.ok(errors.includes('第 1 条货物毛重必须大于 0'))
assert.ok(errors.includes('第 1 条货物需要填写箱型和柜量'))
})
test('requires enabled-party snapshots and order addresses before submission', () => {
const draft = validDraft()
draft.shipperPartyId = ''
draft.shipperPartySnapshot = null
draft.consigneePartySnapshot.address = ''
test('only requires sender for pickup and receiver for a private destination', () => {
const publicDraft = validDraft()
publicDraft.shipperPartySnapshot = null
publicDraft.consigneePartySnapshot = null
assert.deepEqual(validateFirstLegOrderDraft(publicDraft), [])
const errors = validateFirstLegOrderDraft(draft)
assert.ok(errors.includes('请选择发货单位'))
assert.ok(errors.includes('请填写本单收货地址'))
const pickupDraft = validDraft()
pickupDraft.pickupMethod = 'PICKUP'
pickupDraft.shipperPartySnapshot = null
assert.ok(validateFirstLegOrderDraft(pickupDraft).includes('请填写提货联系人'))
const privateDraft = validDraft()
privateDraft.destinationType = 'PRIVATE'
privateDraft.destinationWarehouseId = ''
privateDraft.amazonWarehouseCode = ''
privateDraft.shipmentId = ''
privateDraft.referenceNo = ''
privateDraft.consigneePartySnapshot = null
assert.ok(validateFirstLegOrderDraft(privateDraft).includes('请填写收件人姓名'))
})
test('requires FBA reference, warehouse, shipment, SKU and UPC fields', () => {
+13 -9
View File
@@ -62,7 +62,6 @@ export function createEmptyCargoItem(id = `cargo-${Date.now()}`): FirstLegCargoI
export function validateFirstLegOrderDraft(draft: FirstLegOrderDraft) {
const errors: string[] = []
if (!draft.origin.trim()) errors.push('请填写起运地')
if (!draft.destination.trim()) errors.push('请填写目的地')
if (!/^[A-Za-z]{2}$/.test(draft.destinationCountryCode.trim())) errors.push('请输入两位英文字母的目的国代码')
if (!draft.destinationWarehouseName.trim()) errors.push('请填写目的仓名称')
@@ -70,14 +69,19 @@ export function validateFirstLegOrderDraft(draft: FirstLegOrderDraft) {
if (draft.destinationType === 'PRIVATE' && draft.destinationWarehouseId) errors.push('私人地址不能引用平台公共仓库')
if (!draft.channelCode.trim() || !draft.channelName.trim()) errors.push('请填写渠道编码和渠道名称')
if (!draft.receivingWarehouseId) errors.push('请选择平台收货仓')
if (!draft.shipperPartyId || !draft.shipperPartySnapshot || !draft.shipper.trim()) errors.push('请选择发货单位')
if (draft.shipperPartySnapshot && !draft.shipperPartySnapshot.contactName.trim()) errors.push('请填写发货联系人')
if (draft.shipperPartySnapshot && !draft.shipperPartySnapshot.phone.trim()) errors.push('请填写发货联系电话')
if (draft.shipperPartySnapshot && !draft.shipperPartySnapshot.address.trim()) errors.push('请填写本单发货地址')
if (!draft.consigneePartyId || !draft.consigneePartySnapshot || !draft.consignee.trim()) errors.push('请选择收货单位')
if (draft.consigneePartySnapshot && !draft.consigneePartySnapshot.contactName.trim()) errors.push('请填写收货联系人')
if (draft.consigneePartySnapshot && !draft.consigneePartySnapshot.phone.trim()) errors.push('请填写收货联系电话')
if (draft.consigneePartySnapshot && !draft.consigneePartySnapshot.address.trim()) errors.push('请填写本单收货地址')
if (draft.pickupMethod === 'PICKUP') {
if (!draft.shipperPartySnapshot?.contactName.trim()) errors.push('请填写提货联系人')
if (!draft.shipperPartySnapshot?.phone.trim()) errors.push('请填写提货联系电话')
if (!draft.shipperPartySnapshot?.city.trim()) errors.push('请填写提货城市')
if (!draft.shipperPartySnapshot?.address.trim()) errors.push('请填写提货地址')
}
if (draft.destinationType === 'PRIVATE') {
if (!draft.consigneePartySnapshot?.name.trim()) errors.push('请填写私人地址名称')
if (!draft.consigneePartySnapshot?.contactName.trim()) errors.push('请填写收件人姓名')
if (!draft.consigneePartySnapshot?.phone.trim()) errors.push('请填写收件人电话')
if (!draft.consigneePartySnapshot?.city.trim()) errors.push('请填写收件城市')
if (!draft.consigneePartySnapshot?.address.trim()) errors.push('请填写收件详细地址')
}
if (draft.items.length === 0) errors.push('请至少添加一条货物明细')
if (draft.destinationType === 'FBA') {
@@ -26,7 +26,7 @@ test('exposes real list, draft, submit, and detail workflow entries without canc
assert.match(source, /function submitOrder\(\)/)
assert.match(source, /async function openDetail\(order: FirstLegOrder\)/)
assert.match(source, /Promise\.allSettled\(\[/)
assert.match(source, /store\.load\(\),\s+firstLegPartySource\.load\(\)/)
assert.match(source, /Promise\.allSettled\(\[\s*store\.load\(\),\s*companyRequest/)
assert.match(source, /tenantStore\.loadCompanyContext\(\)/)
assert.match(source, /await store\.loadDetail\(order\.id\)/)
assert.doesNotMatch(source, /cancelOrder|取消订单/)
@@ -98,7 +98,7 @@ test('does not expose arbitrary status mutation controls to customers', () => {
test('requires an enabled platform receiving warehouse in the order form', () => {
assert.match(source, /store\.warehouses\.filter\(\(warehouse\) => warehouse\.warehouseType === 'DOMESTIC'\)/)
assert.match(source, /availableWarehouses\.value\.length === 0/)
assert.match(source, /label="平台收货仓"[^>]*required/)
assert.match(source, /label="国内收货仓"[^>]*required/)
assert.match(source, /v-model="form\.receivingWarehouseId"/)
assert.match(source, /selectedWarehouse\.businessHours/)
assert.match(source, /class="selected-warehouse" aria-label="所选收货仓详情"/)
@@ -129,9 +129,10 @@ test('collects the backend-required first-leg fields', () => {
test('distills order creation into the confirmed prototype sections without fake pricing', () => {
assert.match(source, /<h3>下单信息<\/h3>/)
assert.match(source, /<h3>发货与目的地<\/h3>/)
assert.match(source, /<h3>货物明细<\/h3>/)
assert.doesNotMatch(source, /<h3>发货信息<\/h3>|<h3>目的地信息<\/h3>/)
assert.match(source, /class="segmented-options"/)
assert.match(source, /class="prototype-form-row prototype-form-row--tabs"/)
assert.match(source, /class="cargo-primary-grid"/)
assert.match(source, /title="更多申报信息"/)
assert.doesNotMatch(source, /费用预估|查看最新报价|cost-estimate/)
@@ -149,17 +150,14 @@ test('shows field-level validation and concise guidance for required order input
assert.match(source, /\.order-form :deep\(\.el-form-item__error\).*position: static/)
})
test('selects enabled counterparties and stores editable order snapshots', () => {
assert.match(source, /firstLegPartySource\.listAvailable\('SHIPPER'\)/)
assert.match(source, /firstLegPartySource\.findAvailable\(id, role\)/)
assert.match(source, /firstLegPartySource\.load\(\)/)
assert.match(source, /v-model="form\.shipperPartyId"/)
assert.match(source, /v-model="form\.consigneePartyId"/)
test('collects snapshots only for pickup and private destinations', () => {
assert.doesNotMatch(source, /firstLegPartySource|v-model="form\.(?:shipper|consignee)PartyId"/)
assert.match(source, /form\.pickupMethod === 'PICKUP'/)
assert.match(source, /form\.destinationType === 'PRIVATE'/)
assert.match(source, /form\.shipperPartySnapshot\.contactName/)
assert.match(source, /form\.consigneePartySnapshot\.address/)
assert.match(source, /保存为本订单快照/)
assert.match(source, /form\.shipperPartySnapshot\.name\} · 历史快照/)
assert.match(source, /form\.consigneePartySnapshot\.name\} · 历史快照/)
assert.match(source, /syncPrivateDestinationName/)
assert.match(source, /changePickupMethod/)
})
test('shows fulfillment courier information as read-only client detail', () => {
@@ -180,7 +178,7 @@ test('protects unsaved drafts and exposes recoverable load failures', () => {
assert.match(source, /订单数据加载失败/)
assert.match(source, /订单详情加载失败/)
assert.match(source, /部分订单配套数据加载失败/)
assert.match(source, /往来单位加载失败/)
assert.doesNotMatch(source, /往来单位加载失败/)
assert.match(source, /企业资料加载失败/)
assert.match(source, /@click="loadPageData"/)
assert.match(source, /@click="retryDetail"/)
+111 -130
View File
@@ -10,8 +10,7 @@ import FirstLegOrderEditorShell from '@/modules/first-leg/components/FirstLegOrd
import { useFirstLegOrdersStore } from '@/modules/first-leg/store/firstLegOrders'
import { useTenantAdministrationStore } from '@/modules/tenant-management/public'
import { useAuthStore } from '@/modules/auth/public'
import { createFirstLegPartySnapshot, type FirstLegPartyRole } from '@/modules/first-leg/data/firstLegParties'
import { firstLegPartySource } from '@/modules/first-leg/data/firstLegPartySource'
import type { FirstLegPartyRole, FirstLegPartySnapshot } from '@/modules/first-leg/data/firstLegParties'
import type { FirstLegOrder, FirstLegOrderDraft } from '@/modules/first-leg/types/firstLegOrder'
import {
canClientEditFirstLegOrder,
@@ -36,8 +35,6 @@ const canCreateOrder = computed(() => auth.hasPermission('order:first-leg:create
const canEditOrder = computed(() => auth.hasPermission('order:first-leg:edit'))
const availableWarehouses = computed(() => store.warehouses.filter((warehouse) => warehouse.warehouseType === 'DOMESTIC'))
const availableDestinationWarehouses = computed(() => store.warehouses.filter((warehouse) => warehouse.warehouseType === form.destinationType))
const availableShippers = ref(firstLegPartySource.listAvailable('SHIPPER'))
const availableConsignees = ref(firstLegPartySource.listAvailable('CONSIGNEE'))
const filters = ref<QueryModel>({ keyword: '', status: '', transportMode: '', createdRange: [] })
const appliedFilters = ref<QueryModel>({ ...filters.value })
const queryFields: QueryField[] = [
@@ -112,8 +109,6 @@ function persistPrivateAddresses() {
const orderFormRules: FormRules = {
transportMode: [{ required: true, message: '请选择运输方式', trigger: 'change' }],
businessType: [{ required: true, message: '请选择业务类型', trigger: 'change' }],
origin: [{ required: true, message: '请输入起运地', trigger: 'blur' }],
destination: [{ required: true, message: '请输入目的地', trigger: 'blur' }],
receivingWarehouseId: [{ required: true, message: '请选择平台收货仓', trigger: 'change' }],
destinationCountryCode: [
{ required: true, message: '请输入目的国代码', trigger: 'blur' },
@@ -125,8 +120,23 @@ const orderFormRules: FormRules = {
pickupMethod: [{ required: true, message: '请选择提货方式', trigger: 'change' }],
taxMode: [{ required: true, message: '请选择报关模式', trigger: 'change' }],
destinationType: [{ required: true, message: '请选择目的仓类型', trigger: 'change' }],
shipperPartyId: [{ required: true, message: '请选择发货单位', trigger: 'change' }],
consigneePartyId: [{ required: true, message: '请选择收货单位', trigger: 'change' }],
}
function createEmptyPartySnapshot(role: FirstLegPartyRole): FirstLegPartySnapshot {
return {
id: '',
code: '',
name: '',
role,
contactName: '',
phone: '',
email: '',
countryRegion: role === 'SHIPPER' ? 'CN' : 'US',
province: '',
city: '',
postalCode: '',
address: '',
}
}
function requiredTextRules(message: string): FormItemRule[] {
@@ -231,24 +241,13 @@ async function loadPageData() {
supportingDataLoading.value = true
supportingDataErrors.value = []
companyAuditLoaded.value = false
availableShippers.value = []
availableConsignees.value = []
const companyRequest = canViewCompanyProfile.value
? tenantStore.loadCompanyContext()
: Promise.resolve()
const [ordersResult, partiesResult, companyResult] = await Promise.allSettled([
const [ordersResult, companyResult] = await Promise.allSettled([
store.load(),
firstLegPartySource.load(),
companyRequest,
])
if (partiesResult.status === 'fulfilled') {
availableShippers.value = firstLegPartySource.listAvailable('SHIPPER')
availableConsignees.value = firstLegPartySource.listAvailable('CONSIGNEE')
} else {
supportingDataErrors.value.push(`往来单位加载失败:${errorMessage(partiesResult.reason)}`)
}
if (canViewCompanyProfile.value && companyResult.status === 'fulfilled') {
companyAuditLoaded.value = true
} else if (companyResult.status === 'rejected') {
@@ -344,38 +343,6 @@ function draftFromOrder(order: FirstLegOrder): FirstLegOrderDraft {
}
}
function selectParty(role: FirstLegPartyRole, id: string) {
const party = firstLegPartySource.findAvailable(id, role)
if (!party) return
const snapshot = createFirstLegPartySnapshot(party, role)
if (role === 'SHIPPER') {
form.shipperPartyId = party.id
form.shipperPartySnapshot = snapshot
form.shipper = party.name
form.origin = [snapshot.province, snapshot.city].filter(Boolean).join(' ') || snapshot.countryRegion || snapshot.name
} else {
form.consigneePartyId = party.id
form.consigneePartySnapshot = snapshot
form.consignee = party.name
form.destination = [snapshot.province, snapshot.city].filter(Boolean).join(' ') || snapshot.countryRegion || snapshot.name
if (/^[A-Za-z]{2}$/.test(snapshot.countryRegion)) form.destinationCountryCode = snapshot.countryRegion.toUpperCase()
}
}
function clearParty(role: FirstLegPartyRole) {
if (role === 'SHIPPER') {
form.shipperPartyId = ''
form.shipperPartySnapshot = null
form.shipper = ''
form.origin = ''
} else {
form.consigneePartyId = ''
form.consigneePartySnapshot = null
form.consignee = ''
form.destination = ''
}
}
function selectChannel(code: string) {
const channel = channelOptions.find((item) => item.code === code)
form.channelName = channel?.name ?? ''
@@ -394,11 +361,34 @@ function selectDestinationWarehouse(id: string) {
function changeDestinationType() {
form.destinationWarehouseId = ''
form.destinationWarehouseName = ''
form.destination = ''
form.amazonWarehouseCode = ''
selectedPrivateAddressId.value = ''
form.consigneePartyId = ''
form.consigneePartySnapshot = form.destinationType === 'PRIVATE' ? createEmptyPartySnapshot('CONSIGNEE') : null
form.consignee = ''
resetEditorValidation()
}
function changePickupMethod() {
if (form.pickupMethod === 'PICKUP') {
form.shipperPartySnapshot ??= createEmptyPartySnapshot('SHIPPER')
} else {
form.shipperPartyId = ''
form.shipperPartySnapshot = null
form.shipper = ''
form.origin = ''
}
resetEditorValidation()
}
function syncPrivateDestinationName(value: string) {
if (form.destinationType !== 'PRIVATE') return
form.destinationWarehouseName = value
form.destination = value.trim()
if (form.consigneePartySnapshot) form.consigneePartySnapshot.name = value
}
function selectPrivateAddress(id: string) {
const address = privateAddresses.value.find((item) => item.id === id)
if (!address) return
@@ -412,18 +402,24 @@ function selectPrivateAddress(id: string) {
}
function savePrivateAddress() {
if (!form.consigneePartySnapshot || !form.consigneePartyId || !form.destinationWarehouseName.trim() || !form.destinationCountryCode.trim()) {
ElMessage.warning('请先完整填写收货单位、私人地址名称和目的国代码')
if (!form.consigneePartySnapshot || !form.destinationWarehouseName.trim() || !form.destinationCountryCode.trim()
|| !form.consigneePartySnapshot.contactName.trim() || !form.consigneePartySnapshot.phone.trim()
|| !form.consigneePartySnapshot.city.trim() || !form.consigneePartySnapshot.address.trim()) {
ElMessage.warning('请先完整填写私人地址和收件资料')
return
}
const snapshot = { ...form.consigneePartySnapshot }
const existing = privateAddresses.value.find((item) => item.consigneePartyId === form.consigneePartyId && item.consigneePartySnapshot.address === snapshot.address)
snapshot.name = form.destinationWarehouseName.trim()
snapshot.countryRegion = form.destinationCountryCode.trim().toUpperCase()
const existing = privateAddresses.value.find((item) => item.consigneePartySnapshot.address === snapshot.address)
const partyId = existing?.consigneePartyId || snapshot.id || `private-party-${Date.now()}`
snapshot.id = partyId
const saved: CachedPrivateAddress = {
id: existing?.id ?? globalThis.crypto?.randomUUID?.() ?? `private-${Date.now()}`,
label: `${form.destinationWarehouseName.trim()} · ${snapshot.address}`,
consigneePartyId: form.consigneePartyId,
consigneePartyId: partyId,
consigneePartySnapshot: snapshot,
consignee: form.consignee,
consignee: snapshot.name,
destination: form.destination,
destinationCountryCode: form.destinationCountryCode.trim().toUpperCase(),
destinationWarehouseName: form.destinationWarehouseName.trim(),
@@ -444,22 +440,6 @@ function removePrivateAddress() {
ElMessage.success('已从当前账号的私人地址中移除')
}
function selectShipper(id: string) {
selectParty('SHIPPER', id)
}
function selectConsignee(id: string) {
selectParty('CONSIGNEE', id)
}
function clearShipper() {
clearParty('SHIPPER')
}
function clearConsignee() {
clearParty('CONSIGNEE')
}
async function loadSelectedWarehouse(id: string | number | boolean | undefined) {
const warehouseId = typeof id === 'string' ? id : typeof id === 'number' ? String(id) : ''
if (!warehouseId) return
@@ -744,10 +724,42 @@ async function searchOrders(model: QueryModel) {
<FirstLegOrderEditorShell v-model="editorOpen" :page="isCreatePage" :title="editingId ? '编辑订单草稿' : '创建头程订单'" :before-close="requestCloseEditor" @back="closeEditor">
<el-form ref="orderFormRef" v-loading="isCreatePage && (supportingDataLoading || store.loading)" :model="form" :rules="orderFormRules" label-position="top" class="order-form prototype-order-form" scroll-to-error>
<section class="order-form-section prototype-section">
<div class="order-section-heading"><div><h3>下单信息</h3><p>选择送货方式、收货仓和运输渠道。</p></div></div>
<div class="order-form-grid">
<el-form-item label="取货方式" prop="pickupMethod" required class="form-span-full"><el-radio-group v-model="form.pickupMethod" class="segmented-options"><el-radio-button value="SELF_EXPRESS">自送货 · 快递</el-radio-button><el-radio-button value="PICKUP">我司提货</el-radio-button><el-radio-button value="SELF_TRUCK">自送货 · 卡车</el-radio-button></el-radio-group></el-form-item>
<el-form-item label="平台收货仓" prop="receivingWarehouseId" required class="form-span-full"><el-radio-group v-model="form.receivingWarehouseId" class="warehouse-options" @change="loadSelectedWarehouse"><el-radio-button v-for="warehouse in availableWarehouses" :key="warehouse.id" :value="warehouse.id">{{ warehouse.city ? `${warehouse.name} · ${warehouse.city}` : warehouse.name }}</el-radio-button></el-radio-group></el-form-item>
<div class="order-section-heading"><div><h3>下单信息</h3><p>选择目的仓后,再填写本次运输安排。</p></div></div>
<div class="prototype-form-flow destination-form-flow">
<el-form-item label="目的仓类型" prop="destinationType" required class="prototype-form-row prototype-form-row--tabs"><el-radio-group v-model="form.destinationType" class="segmented-options" @change="changeDestinationType"><el-radio-button value="FBA">FBA 仓</el-radio-button><el-radio-button value="OVERSEAS">海外仓</el-radio-button><el-radio-button value="PRIVATE">私人仓</el-radio-button></el-radio-group></el-form-item>
<el-form-item v-if="form.destinationType === 'PRIVATE'" label="私人地址簿" class="prototype-form-row prototype-form-row--wide address-book-row">
<div class="address-book-control"><el-select v-model="selectedPrivateAddressId" clearable filterable placeholder="选择当前账号保存的私人地址" @change="selectPrivateAddress"><el-option v-for="address in privateAddresses" :key="address.id" :label="address.label" :value="address.id" /></el-select><el-button @click="savePrivateAddress">保存当前地址</el-button><el-button v-if="selectedPrivateAddressId" link type="danger" @click="removePrivateAddress">移除</el-button></div>
</el-form-item>
<el-form-item v-if="form.destinationType !== 'PRIVATE'" :label="form.destinationType === 'FBA' ? 'FBA 仓' : '海外仓'" prop="destinationWarehouseId" required :rules="requiredSelectRules('请选择平台维护的目的仓')" class="prototype-form-row"><el-select v-model="form.destinationWarehouseId" filterable placeholder="请选择当前可送达的目的仓" @change="selectDestinationWarehouse"><el-option v-for="warehouse in availableDestinationWarehouses" :key="warehouse.id" :label="`${warehouse.name} · ${warehouse.code}`" :value="warehouse.id" /></el-select><small v-if="availableDestinationWarehouses.length === 0" class="field-guidance">平台暂未维护可用的{{ form.destinationType === 'FBA' ? ' FBA 仓' : '海外仓' }},请联系平台管理员。</small></el-form-item>
<div v-if="form.destinationType !== 'PRIVATE' && form.destinationWarehouseId" class="selected-destination"><MapPin :size="17" /><span>{{ form.destinationWarehouseName }} · {{ form.amazonWarehouseCode || '海外仓' }} · {{ form.destinationCountryCode }}</span></div>
<template v-if="form.destinationType === 'PRIVATE' && form.consigneePartySnapshot">
<el-form-item label="私人地址名称" prop="destinationWarehouseName" required class="prototype-form-row"><el-input v-model="form.destinationWarehouseName" maxlength="128" placeholder="例如:洛杉矶客户自有仓" @input="syncPrivateDestinationName" /></el-form-item>
<el-form-item label="收件人姓名" prop="consigneePartySnapshot.contactName" required :rules="requiredTextRules('请输入收货联系人姓名')" class="prototype-form-row prototype-form-row--compact"><el-input v-model="form.consigneePartySnapshot.contactName" maxlength="50" placeholder="请输入收件人姓名" /></el-form-item>
<el-form-item label="收件人电话" prop="consigneePartySnapshot.phone" required :rules="requiredTextRules('请输入收货联系电话')" class="prototype-form-row prototype-form-row--compact"><el-input v-model="form.consigneePartySnapshot.phone" maxlength="30" placeholder="请输入收件人电话" /></el-form-item>
<el-form-item label="收件国家代码" prop="destinationCountryCode" required class="prototype-form-row prototype-form-row--short"><el-input v-model="form.destinationCountryCode" maxlength="2" placeholder="例如:US" /></el-form-item>
<el-form-item label="州 / 省" class="prototype-form-row prototype-form-row--compact"><el-input v-model="form.consigneePartySnapshot.province" maxlength="100" placeholder="请输入州或省" /></el-form-item>
<el-form-item label="城市" prop="consigneePartySnapshot.city" required :rules="requiredTextRules('请输入收件城市')" class="prototype-form-row prototype-form-row--compact"><el-input v-model="form.consigneePartySnapshot.city" maxlength="100" placeholder="请输入城市" /></el-form-item>
<el-form-item label="收件地址" prop="consigneePartySnapshot.address" required :rules="requiredTextRules('请输入本单实际收货地址')" class="prototype-form-row prototype-form-row--wide"><el-input v-model="form.consigneePartySnapshot.address" maxlength="300" placeholder="请输入收件详细地址" /></el-form-item>
<el-form-item label="邮编" class="prototype-form-row prototype-form-row--short"><el-input v-model="form.consigneePartySnapshot.postalCode" maxlength="20" placeholder="请输入邮编" /></el-form-item>
</template>
<el-form-item v-if="form.destinationType === 'FBA'" label="货件编号" prop="shipmentId" required :rules="requiredTextRules('请输入 Shipment ID')" class="prototype-form-row prototype-form-row--compact"><el-input v-model="form.shipmentId" maxlength="64" placeholder="Shipment ID" /></el-form-item>
<el-form-item label="跟踪编号" prop="referenceNo" :required="form.destinationType === 'FBA'" :rules="form.destinationType === 'FBA' ? requiredTextRules('FBA 订单请输入 Reference ID') : undefined" class="prototype-form-row prototype-form-row--compact"><el-input v-model="form.referenceNo" maxlength="100" :placeholder="form.destinationType === 'FBA' ? 'Reference ID' : '选填,贵司内部跟踪编号'" /></el-form-item>
<div class="flow-divider" aria-hidden="true"></div>
<el-form-item label="取货方式" prop="pickupMethod" required class="prototype-form-row prototype-form-row--tabs"><el-radio-group v-model="form.pickupMethod" class="segmented-options" @change="changePickupMethod"><el-radio-button value="SELF_EXPRESS">快递自送</el-radio-button><el-radio-button value="PICKUP">我司提货</el-radio-button><el-radio-button value="SELF_TRUCK">卡车自送</el-radio-button></el-radio-group></el-form-item>
<template v-if="form.pickupMethod === 'PICKUP' && form.shipperPartySnapshot">
<el-form-item label="提货联系人" prop="shipperPartySnapshot.contactName" required :rules="requiredTextRules('请输入提货联系人')" class="prototype-form-row prototype-form-row--compact"><el-input v-model="form.shipperPartySnapshot.contactName" maxlength="50" placeholder="请输入联系人姓名" /></el-form-item>
<el-form-item label="联系电话" prop="shipperPartySnapshot.phone" required :rules="requiredTextRules('请输入提货联系电话')" class="prototype-form-row prototype-form-row--compact"><el-input v-model="form.shipperPartySnapshot.phone" maxlength="30" placeholder="请输入联系电话" /></el-form-item>
<el-form-item label="州 / 省" class="prototype-form-row prototype-form-row--compact"><el-input v-model="form.shipperPartySnapshot.province" maxlength="100" placeholder="请输入州或省" /></el-form-item>
<el-form-item label="城市" prop="shipperPartySnapshot.city" required :rules="requiredTextRules('请输入提货城市')" class="prototype-form-row prototype-form-row--compact"><el-input v-model="form.shipperPartySnapshot.city" maxlength="100" placeholder="请输入城市" /></el-form-item>
<el-form-item label="提货地址" prop="shipperPartySnapshot.address" required :rules="requiredTextRules('请输入提货地址')" class="prototype-form-row prototype-form-row--wide"><el-input v-model="form.shipperPartySnapshot.address" maxlength="300" placeholder="请输入详细提货地址" /></el-form-item>
</template>
<el-form-item label="国内收货仓" prop="receivingWarehouseId" required class="prototype-form-row prototype-form-row--wide"><el-radio-group v-model="form.receivingWarehouseId" class="warehouse-options" @change="loadSelectedWarehouse"><el-radio-button v-for="warehouse in availableWarehouses" :key="warehouse.id" :value="warehouse.id">{{ warehouse.city ? `${warehouse.name} · ${warehouse.city}` : warehouse.name }}</el-radio-button></el-radio-group></el-form-item>
</div>
<div v-if="selectedWarehouse" class="selected-warehouse" aria-label="所选收货仓详情"><MapPin :size="17" /><div><span>{{ selectedWarehouse.address ? `${selectedWarehouse.province}${selectedWarehouse.city} · ${selectedWarehouse.address}` : '仓库地址详情暂不可用' }}</span><small>{{ selectedWarehouse.businessHours ? `收货时间:${selectedWarehouse.businessHours}` : '收货时间暂不可用' }}</small></div></div>
<div class="order-form-grid compact-transport-grid">
<el-form-item label="渠道" prop="channelCode" required><el-select v-model="form.channelCode" placeholder="请选择运输渠道" @change="selectChannel"><el-option v-if="form.channelCode && !channelOptions.some((channel) => channel.code === form.channelCode)" :label="form.channelName || form.channelCode" :value="form.channelCode" /><el-option v-for="channel in channelOptions" :key="channel.code" :label="channel.name" :value="channel.code" /></el-select></el-form-item>
<el-form-item label="买单 / 退税" prop="taxMode" required><el-radio-group v-model="form.taxMode"><el-radio value="BUY_DECLARATION">买单报关</el-radio><el-radio value="TAX_REFUND">退税报关</el-radio></el-radio-group></el-form-item>
<el-form-item label="运输方式" prop="transportMode" required><el-radio-group v-model="form.transportMode"><el-radio v-for="item in FIRST_LEG_TRANSPORT_MODES" :key="item.code" :value="item.code">{{ item.label }}</el-radio></el-radio-group></el-form-item>
@@ -755,50 +767,6 @@ async function searchOrders(model: QueryModel) {
<el-form-item label="期望出货日期"><el-date-picker v-model="form.expectedDepartureDate" type="date" value-format="YYYY-MM-DD" placeholder="请选择期望出货日期" /></el-form-item>
<el-form-item label="订单备注"><el-input v-model="form.customerRemark" maxlength="1000" placeholder="选填,例如:入仓前请电话联系" /></el-form-item>
</div>
<div v-if="selectedWarehouse" class="selected-warehouse" aria-label="所选收货仓详情"><MapPin :size="17" /><div><span>{{ selectedWarehouse.address ? `${selectedWarehouse.province}${selectedWarehouse.city} · ${selectedWarehouse.address}` : '仓库地址详情暂不可用' }}</span><small>{{ selectedWarehouse.businessHours ? `收货时间:${selectedWarehouse.businessHours}` : '收货时间暂不可用' }}</small></div></div>
</section>
<section class="order-form-section prototype-section destination-section">
<div class="order-section-heading"><div><h3>发货与目的地</h3><p>单位信息会保存为本订单快照,后续主数据变更不会影响本单。</p></div></div>
<el-form-item label="目的仓类型" prop="destinationType" required><el-radio-group v-model="form.destinationType" class="segmented-options" @change="changeDestinationType"><el-radio-button value="FBA">FBA 仓</el-radio-button><el-radio-button value="OVERSEAS">海外仓</el-radio-button><el-radio-button value="PRIVATE">私人地址</el-radio-button></el-radio-group></el-form-item>
<div v-if="form.destinationType === 'PRIVATE'" class="private-address-toolbar">
<el-select v-model="selectedPrivateAddressId" clearable filterable placeholder="选择当前账号保存的私人地址" @change="selectPrivateAddress">
<el-option v-for="address in privateAddresses" :key="address.id" :label="address.label" :value="address.id" />
</el-select>
<el-button @click="savePrivateAddress">保存当前地址</el-button>
<el-button v-if="selectedPrivateAddressId" type="danger" plain @click="removePrivateAddress">移除缓存</el-button>
</div>
<div class="party-select-grid">
<div class="party-panel">
<h4>发货信息</h4>
<el-alert v-if="availableShippers.length === 0" title="暂无可用发货单位" description="请先在平台往来单位中启用“发货单位”类型。" type="warning" :closable="false" show-icon />
<el-form-item label="发货单位" prop="shipperPartyId" required><el-select v-model="form.shipperPartyId" filterable clearable placeholder="请选择发货单位" @change="selectShipper" @clear="clearShipper"><el-option v-if="form.shipperPartySnapshot && !availableShippers.some((party) => party.id === form.shipperPartyId)" :label="`${form.shipperPartySnapshot.name} · 历史快照`" :value="form.shipperPartyId" disabled /><el-option v-for="party in availableShippers" :key="party.id" :label="`${party.name} · ${party.code}`" :value="party.id" /></el-select></el-form-item>
<div v-if="form.shipperPartySnapshot" class="party-contact-grid">
<el-form-item label="联系人" prop="shipperPartySnapshot.contactName" required :rules="requiredTextRules('请输入发货联系人姓名')"><el-input v-model="form.shipperPartySnapshot.contactName" maxlength="50" placeholder="请输入联系人" /></el-form-item>
<el-form-item label="联系电话" prop="shipperPartySnapshot.phone" required :rules="requiredTextRules('请输入发货联系电话')"><el-input v-model="form.shipperPartySnapshot.phone" maxlength="30" placeholder="请输入联系电话" /></el-form-item>
<el-form-item label="发货地址" prop="shipperPartySnapshot.address" required :rules="requiredTextRules('请输入本单实际发货地址')" class="form-span-full"><el-input v-model="form.shipperPartySnapshot.address" maxlength="300" placeholder="请输入本单实际发货地址" /></el-form-item>
</div>
</div>
<div class="party-panel">
<h4>目的地信息</h4>
<el-alert v-if="availableConsignees.length === 0" title="暂无可用收货单位" description="请先在平台往来单位中启用“收货单位”类型。" type="warning" :closable="false" show-icon />
<el-form-item label="收货单位" prop="consigneePartyId" required><el-select v-model="form.consigneePartyId" filterable clearable placeholder="请选择收货单位" @change="selectConsignee" @clear="clearConsignee"><el-option v-if="form.consigneePartySnapshot && !availableConsignees.some((party) => party.id === form.consigneePartyId)" :label="`${form.consigneePartySnapshot.name} · 历史快照`" :value="form.consigneePartyId" disabled /><el-option v-for="party in availableConsignees" :key="party.id" :label="`${party.name} · ${party.code}`" :value="party.id" /></el-select></el-form-item>
<div v-if="form.consigneePartySnapshot" class="party-contact-grid">
<el-form-item label="收件人" prop="consigneePartySnapshot.contactName" required :rules="requiredTextRules('请输入收货联系人姓名')"><el-input v-model="form.consigneePartySnapshot.contactName" maxlength="50" placeholder="请输入收件人姓名" /></el-form-item>
<el-form-item label="联系电话" prop="consigneePartySnapshot.phone" required :rules="requiredTextRules('请输入收货联系电话')"><el-input v-model="form.consigneePartySnapshot.phone" maxlength="30" placeholder="请输入收件人电话" /></el-form-item>
<el-form-item label="收件地址" prop="consigneePartySnapshot.address" required :rules="requiredTextRules('请输入本单实际收货地址')" class="form-span-full"><el-input v-model="form.consigneePartySnapshot.address" maxlength="300" placeholder="请输入收件详细地址" /></el-form-item>
</div>
</div>
</div>
<div class="destination-fields">
<el-form-item v-if="form.destinationType !== 'PRIVATE'" label="平台目的仓" prop="destinationWarehouseId" required :rules="requiredSelectRules('请选择平台维护的目的仓')"><el-select v-model="form.destinationWarehouseId" filterable placeholder="请选择当前可送达的目的仓" @change="selectDestinationWarehouse"><el-option v-for="warehouse in availableDestinationWarehouses" :key="warehouse.id" :label="`${warehouse.name} · ${warehouse.code}`" :value="warehouse.id" /></el-select><small v-if="availableDestinationWarehouses.length === 0" class="field-guidance">平台暂未维护可用的{{ form.destinationType === 'FBA' ? ' FBA 仓' : '海外仓' }},请联系平台管理员。</small></el-form-item>
<el-form-item label="目的国代码" prop="destinationCountryCode" required><el-input v-model="form.destinationCountryCode" :readonly="form.destinationType !== 'PRIVATE'" maxlength="2" placeholder="例如:US" /></el-form-item>
<el-form-item v-if="form.destinationType === 'FBA'" label="亚马逊仓代码" prop="amazonWarehouseCode" required :rules="requiredTextRules('请选择有效的 FBA 仓')"><el-input v-model="form.amazonWarehouseCode" readonly placeholder="选择 FBA 仓后自动带出" /></el-form-item>
<el-form-item v-else-if="form.destinationType === 'PRIVATE'" label="私人地址名称" prop="destinationWarehouseName" required><el-input v-model="form.destinationWarehouseName" maxlength="128" placeholder="例如:洛杉矶客户自有仓" /></el-form-item>
<el-form-item v-else label="目的仓名称"><el-input v-model="form.destinationWarehouseName" readonly placeholder="选择海外仓后自动带出" /></el-form-item>
<el-form-item v-if="form.destinationType === 'FBA'" label="货件编号" prop="shipmentId" required :rules="requiredTextRules('请输入 Shipment ID')"><el-input v-model="form.shipmentId" maxlength="64" placeholder="Shipment ID" /></el-form-item>
<el-form-item label="跟踪编号" prop="referenceNo" :required="form.destinationType === 'FBA'" :rules="form.destinationType === 'FBA' ? requiredTextRules('FBA 订单请输入 Reference ID') : undefined"><el-input v-model="form.referenceNo" maxlength="100" :placeholder="form.destinationType === 'FBA' ? 'Reference ID' : '选填,贵司内部跟踪编号'" /></el-form-item>
</div>
</section>
<section class="order-form-section prototype-section cargo-section">
@@ -908,7 +876,7 @@ async function searchOrders(model: QueryModel) {
.order-form-section + .order-form-section { margin-top: 24px; padding-top: 22px; border-top: 1px solid var(--border); }
.order-form-section h3 { margin: 0 0 16px; color: var(--navy); font-size: 16px; }
.order-form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 18px; }
.order-form-grid > *, .destination-fields > *, .party-contact-grid > *, .cargo-primary-grid > *, .cargo-secondary-grid > * { min-width: 0; }
.order-form-grid > *, .cargo-primary-grid > *, .cargo-secondary-grid > * { min-width: 0; }
.order-form :deep(.el-select), .order-form :deep(.el-date-editor), .order-form :deep(.el-input-number) { width: 100%; }
.form-span-full { grid-column: 1 / -1; }
.prototype-section { padding: 2px 2px 0; }
@@ -925,14 +893,22 @@ async function searchOrders(model: QueryModel) {
.order-section-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
.order-section-heading h3 { margin-bottom: 5px; }
.order-section-heading p { margin: 0; color: var(--muted); font-size: 11px; }
.party-select-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
.party-panel { min-width: 0; padding: 16px; border: 1px solid var(--border); background: #fbfcfc; }
.party-panel h4 { margin: 0 0 14px; color: var(--navy); font-size: 14px; }
.party-panel :deep(.el-alert) { margin-bottom: 14px; }
.party-contact-grid, .destination-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 12px; }
.destination-fields { margin-top: 16px; }
.private-address-toolbar { display: grid; grid-template-columns: minmax(220px, 1fr) auto auto; gap: 10px; align-items: center; margin-bottom: 16px; }
.prototype-form-flow { display: flex; max-width: 900px; flex-direction: column; padding-top: 4px; }
.prototype-form-row { display: grid; width: 100%; grid-template-columns: 140px minmax(0, 560px); align-items: start; margin-bottom: 18px; }
.prototype-form-row--wide { grid-template-columns: 140px minmax(0, 720px); }
.prototype-form-row--compact { grid-template-columns: 140px minmax(0, 420px); }
.prototype-form-row--short { grid-template-columns: 140px minmax(0, 280px); }
.prototype-form-row--tabs { grid-template-columns: 140px minmax(0, 520px); margin-bottom: 22px; }
.prototype-form-row :deep(.el-form-item__label) { width: auto !important; min-height: 40px; justify-content: flex-end; padding: 0 20px 0 0; line-height: 40px; }
.prototype-form-row :deep(.el-form-item__content) { display: block; min-width: 0; line-height: normal; }
.prototype-form-row :deep(.el-input__wrapper), .prototype-form-row :deep(.el-select__wrapper) { min-height: 40px; }
.address-book-control { display: grid; grid-template-columns: minmax(260px, 1fr) auto auto; gap: 10px; align-items: center; }
.address-book-control :deep(.el-button) { min-height: 40px; margin-left: 0; }
.field-guidance { display: block; margin-top: 5px; color: var(--muted); line-height: 1.45; }
.selected-destination { display: flex; width: min(100%, 720px); align-items: center; gap: 9px; margin: -4px 0 18px 140px; padding: 10px 12px; background: #f2f7f6; color: var(--primary); }
.selected-destination span { color: var(--navy); font-size: 12px; font-weight: 600; }
.flow-divider { width: min(100%, 860px); margin: 4px 0 22px; border-top: 1px solid var(--border); }
.compact-transport-grid { margin-top: 18px; }
.cargo-entry { padding: 16px; border: 1px solid var(--border); background: #fbfcfc; }
.cargo-entry + .cargo-entry { margin-top: 12px; }
.cargo-entry > header { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
@@ -1000,7 +976,12 @@ async function searchOrders(model: QueryModel) {
.order-table-toolbar .el-button { width: 100%; min-height: 40px; }
.order-eligibility { flex-wrap: wrap; }
.order-eligibility .el-button { width: 100%; min-height: 44px; }
.order-form-grid, .order-detail-grid, .party-select-grid, .party-contact-grid, .destination-fields, .private-address-toolbar, .cargo-primary-grid, .cargo-secondary-grid { grid-template-columns: 1fr; }
.order-form-grid, .order-detail-grid, .cargo-primary-grid, .cargo-secondary-grid { grid-template-columns: 1fr; }
.prototype-form-row, .prototype-form-row--wide, .prototype-form-row--compact, .prototype-form-row--short, .prototype-form-row--tabs { display: block; margin-bottom: 18px; }
.prototype-form-row :deep(.el-form-item__label) { min-height: auto; justify-content: flex-start; padding: 0 0 8px; line-height: 1.4; }
.selected-destination { width: 100%; margin-left: 0; }
.address-book-control { grid-template-columns: 1fr auto; }
.address-book-control :deep(.el-select) { grid-column: 1 / -1; }
.segmented-options { width: 100%; align-items: stretch; flex-direction: column; }
.segmented-options :deep(.el-radio-button__inner) { border: 1px solid var(--border); border-radius: 6px !important; box-shadow: none !important; }
.warehouse-options { display: grid; width: 100%; grid-template-columns: 1fr; }