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