This commit is contained in:
2026-08-04 11:14:29 +08:00
parent 5c25806081
commit f533024501
7 changed files with 204 additions and 28 deletions
@@ -24,7 +24,7 @@ test('maps transport and status fields instead of treating container mode as tra
test('uses real API data without falling back to demo warehouses or orders', () => {
assert.match(storeSource, /orders\.value = \[\]/)
assert.match(storeSource, /warehouses\.value = \[\]/)
assert.match(storeSource, /Promise\.all\(\[getMerchantFirstLegOrders\(\), getMerchantWarehouses\(\)\]\)/)
assert.match(storeSource, /Promise\.allSettled\(\[getMerchantFirstLegOrders\(\), getMerchantWarehouses\(\)\]\)/)
assert.doesNotMatch(storeSource, /DEMO_PLATFORM_RECEIVING_WAREHOUSES|initialOrders/)
})
@@ -35,6 +35,13 @@ test('sends backend-required channel, customs, destination and cargo fields', ()
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.match(storeSource, /destinationWarehouseId: draft\.destinationType === 'PRIVATE' \? undefined : draft\.destinationWarehouseId/)
})
test('separates domestic receiving warehouses from public destinations', () => {
assert.match(storeSource, /warehouse\.warehouseType !== 'DOMESTIC'/)
assert.match(storeSource, /item\.warehouseType === draft\.destinationType/)
assert.match(storeSource, /draft\.amazonWarehouseCode = draft\.destinationType === 'FBA' \? destinationWarehouse\.code : ''/)
})
test('only blocks submission when a known company audit status is not active', () => {
@@ -67,6 +74,7 @@ test('maps backend warehouse snapshots and fulfillment courier fields', () => {
test('keeps failed order loads distinct from empty results', () => {
assert.match(storeSource, /const loadError = ref\(''\)/)
assert.match(storeSource, /loadError\.value = error instanceof Error/)
assert.match(storeSource, /return \{ orders, warehouses, loading, loadedFromApi, loadError/)
assert.match(storeSource, /orderResult\.reason instanceof Error/)
assert.match(storeSource, /warehouseLoadError/)
assert.match(storeSource, /return \{ orders, warehouses, loading, loadedFromApi, loadError, warehouseLoadError/)
})
+39 -13
View File
@@ -21,6 +21,12 @@ import type { FirstLegPartySnapshot } from '@/modules/first-leg/data/firstLegPar
import type { FirstLegOrder, FirstLegOrderDraft, FirstLegOrderTimelineItem } from '@/modules/first-leg/types/firstLegOrder'
import { canClientEditFirstLegOrder } from '@/modules/first-leg/utils/firstLegOrder'
export type ClientWarehouseType = 'DOMESTIC' | 'FBA' | 'OVERSEAS'
export type ClientWarehouse = PlatformReceivingWarehouse & {
warehouseType: ClientWarehouseType
countryCode: string
}
const countryCodes: Readonly<Record<string, string>> = {
中国: 'CN', 美国: 'US', 德国: 'DE', 英国: 'GB', 加拿大: 'CA', 日本: 'JP', 澳大利亚: 'AU',
}
@@ -60,7 +66,7 @@ function partyPayload(snapshot: FirstLegPartySnapshot): FirstLegOrderPartyPayloa
}
}
function savePayload(draft: FirstLegOrderDraft, warehouse: PlatformReceivingWarehouse, customerName: string): FirstLegOrderSaveRequest {
function savePayload(draft: FirstLegOrderDraft, warehouse: ClientWarehouse, customerName: string): FirstLegOrderSaveRequest {
if (!draft.shipperPartySnapshot || !draft.consigneePartySnapshot) throw new Error('请完整填写收发货信息')
return {
// 后端把 customerId 作为订单主往来单位校验并以服务端名称覆盖 customerName;
@@ -70,6 +76,7 @@ function savePayload(draft: FirstLegOrderDraft, warehouse: PlatformReceivingWare
transportMode: apiTransport(draft),
tradeTerm: draft.tradeTerm || undefined,
destinationCountryCode: draft.destinationCountryCode.trim().toUpperCase(),
destinationWarehouseId: draft.destinationType === 'PRIVATE' ? undefined : draft.destinationWarehouseId,
destinationWarehouseName: draft.destinationWarehouseName,
expectedShipDate: draft.expectedDepartureDate || undefined,
customerRemark: draft.customerRemark || undefined,
@@ -167,26 +174,29 @@ function mapOrder(order: MerchantFirstLegOrder, warehouses: PlatformReceivingWar
})),
timeline: detail ? timeline(detail) : [], createdAt: order.createdAt, updatedAt: order.updatedAt,
tradeTerm: order.tradeTerm ?? '', destinationCountryCode: order.destinationCountryCode,
destinationWarehouseId: order.destinationWarehouseId ? String(order.destinationWarehouseId) : '',
destinationWarehouseName: order.destinationWarehouseName, pickupMethod: order.pickupMethod, channelCode: order.channelCode,
channelName: order.channelName, taxMode: order.taxMode, customsInspection: order.customsInspection,
destinationType: order.destinationType, amazonWarehouseCode: order.amazonWarehouseCode ?? '', shipmentId: order.shipmentId ?? '',
}
}
function mapWarehouse(item: MerchantWarehouse): PlatformReceivingWarehouse {
function mapWarehouse(item: MerchantWarehouse): ClientWarehouse {
return {
id: String(item.id), code: item.warehouseCode, name: item.warehouseName, province: '',
city: '', address: '', contactName: item.contactName ?? '', contactPhone: item.contactPhone ?? '',
businessHours: '', status: item.status, remark: item.remark ?? '', updatedAt: item.updatedAt ?? '',
warehouseType: item.warehouseType as ClientWarehouseType, countryCode: item.countryCode,
}
}
export const useFirstLegOrdersStore = defineStore('client-first-leg-orders', () => {
const orders = ref<FirstLegOrder[]>([])
const warehouses = ref<PlatformReceivingWarehouse[]>([])
const warehouses = ref<ClientWarehouse[]>([])
const loading = ref(false)
const loadedFromApi = ref(false)
const loadError = ref('')
const warehouseLoadError = ref('')
const activeCount = computed(() => orders.value.filter((item) => ['PENDING_REVIEW', 'ACCEPTED', 'BOOKED', 'IN_TRANSIT'].includes(item.status)).length)
const pendingCount = computed(() => orders.value.filter((item) => item.status === 'PENDING_REVIEW').length)
@@ -196,16 +206,23 @@ export const useFirstLegOrdersStore = defineStore('client-first-leg-orders', ()
loading.value = true
loadedFromApi.value = true
loadError.value = ''
warehouseLoadError.value = ''
orders.value = []
warehouses.value = []
try {
const [orderPage, warehousePage] = await Promise.all([getMerchantFirstLegOrders(), getMerchantWarehouses()])
warehouses.value = warehousePage.records.filter((item) => item.status === 'ACTIVE').map(mapWarehouse)
orders.value = orderPage.records.map((item) => mapOrder(item, warehouses.value))
} catch (error) {
loadError.value = error instanceof Error ? error.message : '订单与仓库数据加载失败'
throw error
} finally { loading.value = false }
const [orderResult, warehouseResult] = await Promise.allSettled([getMerchantFirstLegOrders(), getMerchantWarehouses()])
if (warehouseResult.status === 'fulfilled') {
warehouses.value = warehouseResult.value.records
.filter((item) => item.status === 'ACTIVE' && ['DOMESTIC', 'FBA', 'OVERSEAS'].includes(item.warehouseType))
.map(mapWarehouse)
} else {
warehouseLoadError.value = warehouseResult.reason instanceof Error ? warehouseResult.reason.message : '公共仓库数据加载失败'
}
if (orderResult.status === 'fulfilled') {
orders.value = orderResult.value.records.map((item) => mapOrder(item, warehouses.value))
} else {
loadError.value = orderResult.reason instanceof Error ? orderResult.reason.message : '订单数据加载失败'
}
loading.value = false
}
async function loadDetail(id: string) {
@@ -235,7 +252,16 @@ export const useFirstLegOrdersStore = defineStore('client-first-leg-orders', ()
async function saveDraft(draft: FirstLegOrderDraft, customerName: string, id?: string) {
const warehouse = warehouses.value.find((item) => item.id === draft.receivingWarehouseId)
if (!warehouse) throw new Error('所选收货仓当前不可用')
if (!warehouse || warehouse.warehouseType !== 'DOMESTIC') throw new Error('所选国内收货仓当前不可用')
if (draft.destinationType !== 'PRIVATE') {
const destinationWarehouse = warehouses.value.find((item) => item.id === draft.destinationWarehouseId && item.warehouseType === draft.destinationType)
if (!destinationWarehouse) throw new Error('所选目的仓当前不可用,请重新选择')
draft.destinationWarehouseName = destinationWarehouse.name
draft.destinationCountryCode = destinationWarehouse.countryCode
draft.amazonWarehouseCode = draft.destinationType === 'FBA' ? destinationWarehouse.code : ''
} else {
draft.destinationWarehouseId = ''
}
if (id) {
const order = getById(id)
if (!order || !canClientEditFirstLegOrder(order.status)) throw new Error('当前订单状态不允许编辑')
@@ -253,5 +279,5 @@ export const useFirstLegOrdersStore = defineStore('client-first-leg-orders', ()
return loadDetail(order.id)
}
return { orders, warehouses, loading, loadedFromApi, loadError, activeCount, pendingCount, getById, load, loadDetail, loadWarehouseDetail, saveDraft, submit }
return { orders, warehouses, loading, loadedFromApi, loadError, warehouseLoadError, activeCount, pendingCount, getById, load, loadDetail, loadWarehouseDetail, saveDraft, submit }
})
@@ -70,6 +70,7 @@ export interface FirstLegOrder {
updatedAt: string
tradeTerm: string
destinationCountryCode: string
destinationWarehouseId: string
destinationWarehouseName: string
pickupMethod: 'SELF_EXPRESS' | 'PICKUP' | 'SELF_TRUCK'
channelCode: string
@@ -101,6 +102,7 @@ export type FirstLegOrderDraft = Pick<
| 'items'
| 'tradeTerm'
| 'destinationCountryCode'
| 'destinationWarehouseId'
| 'destinationWarehouseName'
| 'pickupMethod'
| 'channelCode'
@@ -41,6 +41,7 @@ function validDraft() {
customerRemark: '',
tradeTerm: 'FOB',
destinationCountryCode: 'US',
destinationWarehouseId: 'WH-FBA-ONT8',
destinationWarehouseName: 'ONT8',
pickupMethod: 'SELF_EXPRESS',
channelCode: 'SEA-US-FBA',
@@ -113,6 +114,20 @@ test('requires FBA reference, warehouse, shipment, SKU and UPC fields', () => {
assert.ok(errors.includes('第 1 条货物缺少 UPC'))
})
test('requires public destination ids while keeping private addresses detached from public warehouses', () => {
const publicDraft = validDraft()
publicDraft.destinationWarehouseId = ''
assert.ok(validateFirstLegOrderDraft(publicDraft).includes('请选择平台维护的目的仓'))
const privateDraft = validDraft()
privateDraft.destinationType = 'PRIVATE'
privateDraft.destinationWarehouseId = 'WH-FBA-ONT8'
privateDraft.amazonWarehouseCode = ''
privateDraft.shipmentId = ''
privateDraft.referenceNo = ''
assert.ok(validateFirstLegOrderDraft(privateDraft).includes('私人地址不能引用平台公共仓库'))
})
test('does not require container details for non-FCL cargo', () => {
const draft = validDraft()
draft.transportMode = 'AIR'
@@ -66,6 +66,8 @@ export function validateFirstLegOrderDraft(draft: FirstLegOrderDraft) {
if (!draft.destination.trim()) errors.push('请填写目的地')
if (!/^[A-Za-z]{2}$/.test(draft.destinationCountryCode.trim())) errors.push('请输入两位英文字母的目的国代码')
if (!draft.destinationWarehouseName.trim()) errors.push('请填写目的仓名称')
if (draft.destinationType !== 'PRIVATE' && !draft.destinationWarehouseId) errors.push('请选择平台维护的目的仓')
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('请选择发货单位')
@@ -96,7 +96,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, /computed\(\(\) => store\.warehouses\)/)
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, /v-model="form\.receivingWarehouseId"/)
@@ -111,6 +111,15 @@ test('requires an enabled platform receiving warehouse in the order form', () =>
assert.match(source, /loadSelectedWarehouse\(detailedOrder\.receivingWarehouseId\)/)
})
test('selects platform destinations by type and caches private addresses per user', () => {
assert.match(source, /warehouse\.warehouseType === form\.destinationType/)
assert.match(source, /v-model="form\.destinationWarehouseId"/)
assert.match(source, /form\.amazonWarehouseCode = form\.destinationType === 'FBA' \? warehouse\.code : ''/)
assert.match(source, /cc:first-leg:private-addresses:\$\{auth\.user\?\.id/)
assert.match(source, /localStorage\.setItem\(privateAddressStorageKey\(\)/)
assert.match(source, /私人地址不能引用平台公共仓库|form\.destinationWarehouseId = ''/)
})
test('collects the backend-required first-leg fields', () => {
for (const model of ['destinationCountryCode', 'destinationWarehouseName', 'channelCode', 'pickupMethod', 'taxMode', 'destinationType', 'packageType', 'upc']) {
assert.match(source, new RegExp(`v-model="(?:form|item)\\.${model}"`))
@@ -191,4 +200,5 @@ test('loads complete order detail before editing and rejects an unavailable hist
assert.match(source, /draftFromOrder\(detailedOrder\)/)
assert.match(source, /draft\.receivingWarehouseId = ''/)
assert.match(source, /原平台收货仓当前不可用,请重新选择可用收货仓/)
assert.match(source, /原目的仓当前不可用,请重新选择平台维护的目的仓/)
})
@@ -34,7 +34,8 @@ const companyAuditLoaded = ref(false)
const companySubmissionBlocked = computed(() => companyAuditLoaded.value && tenantStore.company.auditStatus !== 'ACTIVE')
const canCreateOrder = computed(() => auth.hasPermission('order:first-leg:create'))
const canEditOrder = computed(() => auth.hasPermission('order:first-leg:edit'))
const availableWarehouses = computed(() => store.warehouses)
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: [] })
@@ -77,6 +78,37 @@ const supportingDataErrors = ref<string[]>([])
const editorBaseline = ref('')
let cargoSequence = 0
interface CachedPrivateAddress {
id: string
label: string
consigneePartyId: string
consigneePartySnapshot: NonNullable<FirstLegOrderDraft['consigneePartySnapshot']>
consignee: string
destination: string
destinationCountryCode: string
destinationWarehouseName: string
}
const privateAddresses = ref<CachedPrivateAddress[]>([])
const selectedPrivateAddressId = ref('')
function privateAddressStorageKey() {
return `cc:first-leg:private-addresses:${auth.user?.id ?? 'anonymous'}`
}
function loadPrivateAddresses() {
try {
const value = JSON.parse(localStorage.getItem(privateAddressStorageKey()) ?? '[]') as unknown
privateAddresses.value = Array.isArray(value) ? value.filter((item): item is CachedPrivateAddress => Boolean(item && typeof item === 'object' && 'id' in item && 'consigneePartySnapshot' in item)) : []
} catch {
privateAddresses.value = []
}
}
function persistPrivateAddresses() {
localStorage.setItem(privateAddressStorageKey(), JSON.stringify(privateAddresses.value))
}
const orderFormRules: FormRules = {
transportMode: [{ required: true, message: '请选择运输方式', trigger: 'change' }],
businessType: [{ required: true, message: '请选择业务类型', trigger: 'change' }],
@@ -141,6 +173,7 @@ function emptyDraft(): FirstLegOrderDraft {
customerRemark: '',
tradeTerm: 'FOB',
destinationCountryCode: 'US',
destinationWarehouseId: '',
destinationWarehouseName: '',
pickupMethod: 'SELF_EXPRESS',
channelCode: 'SEA-US-FBA',
@@ -221,9 +254,8 @@ async function loadPageData() {
} else if (companyResult.status === 'rejected') {
supportingDataErrors.value.push(`企业资料加载失败:${errorMessage(companyResult.reason)}`)
}
if (ordersResult.status === 'rejected' && !store.loadError) {
supportingDataErrors.value.push(`订单与仓库加载失败:${errorMessage(ordersResult.reason)}`)
}
if (ordersResult.status === 'rejected' && !store.loadError) supportingDataErrors.value.push(`订单加载失败:${errorMessage(ordersResult.reason)}`)
if (store.warehouseLoadError) supportingDataErrors.value.push(`公共仓库加载失败:${store.warehouseLoadError}`)
supportingDataLoading.value = false
}
@@ -239,6 +271,7 @@ function handleBeforeUnload(event: BeforeUnloadEvent) {
onMounted(() => {
window.addEventListener('beforeunload', handleBeforeUnload)
loadPrivateAddresses()
void loadPageData()
})
@@ -297,6 +330,7 @@ function draftFromOrder(order: FirstLegOrder): FirstLegOrderDraft {
customerRemark: order.customerRemark,
tradeTerm: order.tradeTerm,
destinationCountryCode: order.destinationCountryCode,
destinationWarehouseId: order.destinationWarehouseId,
destinationWarehouseName: order.destinationWarehouseName,
pickupMethod: order.pickupMethod,
channelCode: order.channelCode,
@@ -347,8 +381,67 @@ function selectChannel(code: string) {
form.channelName = channel?.name ?? ''
}
function syncAmazonWarehouse(value: string) {
if (form.destinationType === 'FBA') form.destinationWarehouseName = value.trim()
function selectDestinationWarehouse(id: string) {
const warehouse = availableDestinationWarehouses.value.find((item) => item.id === id)
if (!warehouse) return
form.destinationWarehouseId = warehouse.id
form.destinationWarehouseName = warehouse.name
form.destinationCountryCode = warehouse.countryCode.toUpperCase()
form.destination = warehouse.name
form.amazonWarehouseCode = form.destinationType === 'FBA' ? warehouse.code : ''
}
function changeDestinationType() {
form.destinationWarehouseId = ''
form.destinationWarehouseName = ''
form.amazonWarehouseCode = ''
selectedPrivateAddressId.value = ''
resetEditorValidation()
}
function selectPrivateAddress(id: string) {
const address = privateAddresses.value.find((item) => item.id === id)
if (!address) return
form.consigneePartyId = address.consigneePartyId
form.consigneePartySnapshot = { ...address.consigneePartySnapshot }
form.consignee = address.consignee
form.destination = address.destination
form.destinationCountryCode = address.destinationCountryCode
form.destinationWarehouseId = ''
form.destinationWarehouseName = address.destinationWarehouseName
}
function savePrivateAddress() {
if (!form.consigneePartySnapshot || !form.consigneePartyId || !form.destinationWarehouseName.trim() || !form.destinationCountryCode.trim()) {
ElMessage.warning('请先完整填写收货单位、私人地址名称和目的国代码')
return
}
const snapshot = { ...form.consigneePartySnapshot }
const existing = privateAddresses.value.find((item) => item.consigneePartyId === form.consigneePartyId && item.consigneePartySnapshot.address === snapshot.address)
const saved: CachedPrivateAddress = {
id: existing?.id ?? globalThis.crypto?.randomUUID?.() ?? `private-${Date.now()}`,
label: `${form.destinationWarehouseName.trim()} · ${snapshot.address}`,
consigneePartyId: form.consigneePartyId,
consigneePartySnapshot: snapshot,
consignee: form.consignee,
destination: form.destination,
destinationCountryCode: form.destinationCountryCode.trim().toUpperCase(),
destinationWarehouseName: form.destinationWarehouseName.trim(),
}
privateAddresses.value = existing
? privateAddresses.value.map((item) => item.id === existing.id ? saved : item)
: [saved, ...privateAddresses.value]
selectedPrivateAddressId.value = saved.id
persistPrivateAddresses()
ElMessage.success(existing ? '私人地址已更新' : '私人地址已保存到当前账号')
}
function removePrivateAddress() {
if (!selectedPrivateAddressId.value) return
privateAddresses.value = privateAddresses.value.filter((item) => item.id !== selectedPrivateAddressId.value)
selectedPrivateAddressId.value = ''
persistPrivateAddresses()
ElMessage.success('已从当前账号的私人地址中移除')
}
function selectShipper(id: string) {
@@ -399,6 +492,15 @@ async function openEdit(order: FirstLegOrder) {
draft.receivingWarehouseId = ''
ElMessage.warning('原平台收货仓当前不可用,请重新选择可用收货仓')
}
if (draft.destinationType !== 'PRIVATE') {
const destinationAvailable = store.warehouses.some((item) => item.id === draft.destinationWarehouseId && item.warehouseType === draft.destinationType)
if (!destinationAvailable) {
draft.destinationWarehouseId = ''
draft.destinationWarehouseName = ''
draft.amazonWarehouseCode = ''
ElMessage.warning('原目的仓当前不可用,请重新选择平台维护的目的仓')
}
}
editingId.value = detailedOrder.id
replaceForm(draft)
if (warehouseAvailable) void loadSelectedWarehouse(detailedOrder.receivingWarehouseId)
@@ -658,7 +760,14 @@ async function searchOrders(model: QueryModel) {
<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"><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 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>
@@ -682,9 +791,11 @@ async function searchOrders(model: QueryModel) {
</div>
</div>
<div class="destination-fields">
<el-form-item label="目的国代码" prop="destinationCountryCode" required><el-input v-model="form.destinationCountryCode" maxlength="2" placeholder="例如:US" /></el-form-item>
<el-form-item v-if="form.destinationType === 'FBA'" label="亚马逊仓代码" prop="amazonWarehouseCode" required :rules="requiredTextRules('请输入 Amazon 仓库编码')"><el-input v-model="form.amazonWarehouseCode" maxlength="32" placeholder="例如:ONT8" @input="syncAmazonWarehouse" /></el-form-item>
<el-form-item v-else label="目的仓名称" prop="destinationWarehouseName" required><el-input v-model="form.destinationWarehouseName" maxlength="128" placeholder="请输入海外仓或私人仓名称" /></el-form-item>
<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>
@@ -820,6 +931,8 @@ async function searchOrders(model: QueryModel) {
.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; }
.field-guidance { display: block; margin-top: 5px; color: var(--muted); line-height: 1.45; }
.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; }
@@ -887,7 +1000,7 @@ 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, .cargo-primary-grid, .cargo-secondary-grid { grid-template-columns: 1fr; }
.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; }
.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; }