优化
This commit is contained in:
@@ -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"/)
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user