feat(client): align account and order foundations
This commit is contained in:
@@ -279,15 +279,20 @@ 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', () => {
|
||||
test('selects platform destinations and uses customer-scoped server address books', () => {
|
||||
assert.match(source, /warehouse\.warehouseType === form\.destinationType/)
|
||||
assert.match(source, /await store\.loadWarehouseType\(type\)/)
|
||||
assert.match(source, /await loadDestinationWarehouses\(form\.destinationType\)/)
|
||||
assert.match(source, /:loading="destinationWarehousesLoading"/)
|
||||
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, /getMerchantOrderAddresses\(\)/)
|
||||
assert.match(source, /createMerchantOrderAddress\(payload\)/)
|
||||
assert.match(source, /updateMerchantOrderAddress\(form\.(?:senderAddressId|privateDestinationAddressId), payload\)/)
|
||||
assert.match(source, /disableMerchantOrderAddress\(id\)/)
|
||||
assert.match(source, /v-model="form\.senderAddressId"/)
|
||||
assert.match(source, /v-model="form\.privateDestinationAddressId"/)
|
||||
assert.doesNotMatch(source, /localStorage|cc:first-leg:private-addresses/)
|
||||
assert.match(source, /私人地址不能引用平台公共仓库|form\.destinationWarehouseId = ''/)
|
||||
})
|
||||
|
||||
@@ -318,6 +323,8 @@ test('collects the backend-required first-leg fields', () => {
|
||||
assert.match(source, new RegExp(`v-model="(?:form|item)\\.${model}"`))
|
||||
}
|
||||
assert.match(source, /form\.channelName = channel\?\.channelName/)
|
||||
assert.match(source, /v-model="item\.woodenProduct"/)
|
||||
assert.match(source, /货物包含木制品,提交前必须选择“是”/)
|
||||
})
|
||||
|
||||
test('distills order creation into the confirmed prototype sections with server pricing', () => {
|
||||
@@ -330,7 +337,11 @@ test('distills order creation into the confirmed prototype sections with server
|
||||
assert.match(source, /class="segmented-options"/)
|
||||
assert.match(source, /class="prototype-form-row prototype-form-row--tabs"/)
|
||||
assert.match(source, /<table class="cargo-edit-table">/)
|
||||
assert.match(source, /上传 Excel[\s\S]*下载模板[\s\S]*添加行[\s\S]*删除所选行/)
|
||||
assert.match(source, /批量导入订单/)
|
||||
assert.match(source, /下载正式模板[\s\S]*选择 Excel/)
|
||||
assert.match(source, /仅支持 \.xlsx,文件不超过 5MB、最多 50 行/)
|
||||
assert.match(source, /添加行[\s\S]*删除所选行/)
|
||||
assert.doesNotMatch(source, /货物明细导入模板\.xls/)
|
||||
for (const heading of ['SKU NO.', '中文品名', '箱规长 cm', '总毛重', '报关 HS CODE', '货件编号', '跟踪编号', 'UPC NO.']) {
|
||||
assert.ok(cargoTableSource.includes(heading))
|
||||
}
|
||||
|
||||
@@ -16,11 +16,22 @@ import { getMerchantInboundBilling } from '@/modules/first-leg/api/inboundBillin
|
||||
import { getMerchantCustomsForOrder, isMerchantCustomsUnavailable, type MerchantCustomsContainer } from '@/modules/first-leg/api/customs'
|
||||
import {
|
||||
estimateMerchantFirstLegPrice,
|
||||
createIdempotencyKey,
|
||||
createMerchantOrderAddress,
|
||||
disableMerchantOrderAddress,
|
||||
downloadMerchantFirstLegBatchImportTemplate,
|
||||
downloadMerchantOrderCartonLabelsBatch,
|
||||
downloadMerchantProductImage,
|
||||
getMerchantInboundCodePolicy,
|
||||
getMerchantOrderAddresses,
|
||||
getMerchantPricingChannels,
|
||||
importMerchantFirstLegOrders,
|
||||
updateMerchantOrderAddress,
|
||||
uploadMerchantProductImage,
|
||||
type CustomerOrderAddress,
|
||||
type CustomerOrderAddressSaveRequest,
|
||||
type MerchantBatchImportMode,
|
||||
type MerchantBatchImportResponse,
|
||||
type MerchantPriceEstimate,
|
||||
type MerchantPriceEstimateRequest,
|
||||
type MerchantPricingChannel,
|
||||
@@ -83,18 +94,14 @@ const priceEstimate = ref<MerchantPriceEstimate | null>(null)
|
||||
const priceEstimateLoading = ref(false)
|
||||
const priceEstimateError = ref('')
|
||||
const selectedCargoIds = ref<string[]>([])
|
||||
const cargoImportInput = ref<HTMLInputElement>()
|
||||
|
||||
const cargoTemplateColumns = [
|
||||
['SKU NO.', 'sku'], ['中文品名', 'cargoNameCn'], ['英文品名', 'cargoNameEn'], ['报关 HS CODE', 'hsCode'], ['清关 HS CODE', 'customsHsCode'],
|
||||
['包装类型', 'packageType'], ['箱规长 cm', 'lengthCm'], ['箱规宽 cm', 'widthCm'], ['箱规高 cm', 'heightCm'], ['每箱数量', 'unitsPerPackage'],
|
||||
['箱数', 'packageCount'], ['总毛重', 'grossWeightKg'], ['总体积', 'volumeCbm'], ['总数量', 'pieces'], ['总净重', 'netWeightKg'],
|
||||
['清关申报单价 USD', 'unitDeclaredValue'], ['清关申报金额 USD', 'declaredValue'], ['材质', 'material'], ['用途', 'usage'], ['型号', 'model'],
|
||||
['品牌', 'brand'], ['产品长 cm', 'productLengthCm'], ['产品宽 cm', 'productWidthCm'], ['产品高 cm', 'productHeightCm'], ['危险品', 'dangerousGoods'],
|
||||
['货件编号', 'shipmentId'], ['跟踪编号', 'referenceId'], ['UPC NO.', 'upc'], ['备注', 'remark'],
|
||||
] as const
|
||||
|
||||
const cargoNumberKeys = new Set<string>(['lengthCm', 'widthCm', 'heightCm', 'unitsPerPackage', 'packageCount', 'grossWeightKg', 'volumeCbm', 'pieces', 'netWeightKg', 'unitDeclaredValue', 'declaredValue', 'productLengthCm', 'productWidthCm', 'productHeightCm'])
|
||||
const batchImportInput = ref<HTMLInputElement>()
|
||||
const batchImportOpen = ref(false)
|
||||
const batchImportMode = ref<MerchantBatchImportMode>('PARTIAL')
|
||||
const batchImportFile = ref<File | null>(null)
|
||||
const batchImportIdempotencyKey = ref('')
|
||||
const batchImportSubmitting = ref(false)
|
||||
const batchTemplateDownloading = ref(false)
|
||||
const batchImportResult = ref<MerchantBatchImportResponse | null>(null)
|
||||
const fixedOrderColumns: DataTableColumn[] = [
|
||||
{ key: 'selection', type: 'selection', width: 48, fixed: 'left', reserveSelection: true },
|
||||
{ key: 'actions', label: '操作', width: 216, fixed: 'left', align: 'center', headerAlign: 'center', slotName: 'actions' },
|
||||
@@ -207,35 +214,13 @@ let channelRequestSequence = 0
|
||||
let estimateRequestSequence = 0
|
||||
let estimateTimer: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
interface CachedPrivateAddress {
|
||||
id: string
|
||||
label: string
|
||||
consigneePartyId: string
|
||||
consigneePartySnapshot: NonNullable<FirstLegOrderDraft['consigneePartySnapshot']>
|
||||
consignee: string
|
||||
destination: string
|
||||
destinationCountryCode: string
|
||||
destinationWarehouseName: string
|
||||
}
|
||||
const orderAddresses = ref<CustomerOrderAddress[]>([])
|
||||
const orderAddressesLoading = ref(false)
|
||||
const pickupAddresses = computed(() => orderAddresses.value.filter((item) => item.addressType === 'PICKUP'))
|
||||
const privateAddresses = computed(() => orderAddresses.value.filter((item) => item.addressType === 'PRIVATE_DESTINATION'))
|
||||
|
||||
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))
|
||||
function addressLabel(address: CustomerOrderAddress) {
|
||||
return `${address.addressName} · ${address.contactName} · ${address.countryCode} ${address.city} ${address.addressLine}`
|
||||
}
|
||||
|
||||
const orderFormRules: FormRules = {
|
||||
@@ -352,9 +337,11 @@ function emptyDraft(): FirstLegOrderDraft {
|
||||
receivingWarehouseId: '',
|
||||
expectedDepartureDate: '',
|
||||
shipperPartyId: '',
|
||||
senderAddressId: '',
|
||||
shipperPartySnapshot: null,
|
||||
shipper: '',
|
||||
consigneePartyId: '',
|
||||
privateDestinationAddressId: '',
|
||||
consigneePartySnapshot: null,
|
||||
consignee: '',
|
||||
priority: 'NORMAL',
|
||||
@@ -368,7 +355,7 @@ function emptyDraft(): FirstLegOrderDraft {
|
||||
channelName: '',
|
||||
pricingTaxMode: 'TAX_INCLUDED',
|
||||
taxMode: 'BUY_DECLARATION',
|
||||
customsInspection: false,
|
||||
customsInspection: null,
|
||||
destinationType: 'FBA',
|
||||
amazonWarehouseCode: '',
|
||||
shipmentId: '',
|
||||
@@ -566,11 +553,13 @@ async function loadPageData() {
|
||||
const orderPageRequest = isCreatePage.value
|
||||
? store.loadWarehouseType('DOMESTIC', true)
|
||||
: store.load({ includeDrafts: true })
|
||||
const [ordersResult, companyResult, destinationResult, policyResult] = await Promise.allSettled([
|
||||
const addressRequest = isCreatePage.value ? loadOrderAddresses() : Promise.resolve()
|
||||
const [ordersResult, companyResult, destinationResult, policyResult, addressResult] = await Promise.allSettled([
|
||||
orderPageRequest,
|
||||
companyRequest,
|
||||
destinationRequest,
|
||||
getMerchantInboundCodePolicy(),
|
||||
addressRequest,
|
||||
])
|
||||
if (policyResult.status === 'fulfilled') inboundCodePolicy.value = policyResult.value
|
||||
else {
|
||||
@@ -586,6 +575,7 @@ async function loadPageData() {
|
||||
if (store.warehouseLoadError) supportingDataErrors.value.push(`公共仓库加载失败:${store.warehouseLoadError}`)
|
||||
if (!isCreatePage.value && store.draftLoadError) supportingDataErrors.value.push(`订单草稿加载失败:${store.draftLoadError}`)
|
||||
if (destinationResult.status === 'rejected') supportingDataErrors.value.push(`目的仓加载失败:${errorMessage(destinationResult.reason)}`)
|
||||
if (addressResult.status === 'rejected') supportingDataErrors.value.push(`企业地址簿加载失败:${errorMessage(addressResult.reason)}`)
|
||||
await autofillWarehouseSelections()
|
||||
if (isCreatePage.value && routeDraftId.value) await loadRouteDraft(routeDraftId.value)
|
||||
supportingDataLoading.value = false
|
||||
@@ -625,7 +615,6 @@ function getOrderCargoSummary(order: FirstLegOrder) {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadPrivateAddresses()
|
||||
void loadPageData()
|
||||
})
|
||||
|
||||
@@ -745,9 +734,11 @@ function draftFromOrder(order: FirstLegOrder): FirstLegOrderDraft {
|
||||
receivingWarehouseId: order.receivingWarehouseId,
|
||||
expectedDepartureDate: order.expectedDepartureDate,
|
||||
shipperPartyId: order.shipperPartyId,
|
||||
senderAddressId: '',
|
||||
shipperPartySnapshot: order.shipperPartySnapshot ? { ...order.shipperPartySnapshot } : null,
|
||||
shipper: order.shipper,
|
||||
consigneePartyId: order.consigneePartyId,
|
||||
privateDestinationAddressId: '',
|
||||
consigneePartySnapshot: order.consigneePartySnapshot ? { ...order.consigneePartySnapshot } : null,
|
||||
consignee: order.consignee,
|
||||
priority: order.priority,
|
||||
@@ -939,7 +930,7 @@ async function changeDestinationType() {
|
||||
form.destinationWarehouseName = ''
|
||||
form.destination = ''
|
||||
form.amazonWarehouseCode = ''
|
||||
selectedPrivateAddressId.value = ''
|
||||
form.privateDestinationAddressId = ''
|
||||
form.consigneePartyId = ''
|
||||
form.consigneePartySnapshot = form.destinationType === 'PRIVATE' ? createEmptyPartySnapshot('CONSIGNEE') : null
|
||||
form.consignee = ''
|
||||
@@ -958,6 +949,7 @@ function changePickupMethod() {
|
||||
if (form.pickupMethod === 'PICKUP') {
|
||||
form.shipperPartySnapshot ??= createPickupPartySnapshot()
|
||||
} else {
|
||||
form.senderAddressId = ''
|
||||
form.shipperPartyId = ''
|
||||
form.shipperPartySnapshot = null
|
||||
form.shipper = ''
|
||||
@@ -974,54 +966,129 @@ function syncPrivateDestinationName(value: string) {
|
||||
}
|
||||
|
||||
function selectPrivateAddress(id: string) {
|
||||
const address = privateAddresses.value.find((item) => item.id === id)
|
||||
const address = privateAddresses.value.find((item) => String(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.privateDestinationAddressId = String(address.id)
|
||||
form.consigneePartyId = ''
|
||||
form.consigneePartySnapshot = addressSnapshot(address, 'CONSIGNEE')
|
||||
form.consignee = address.companyName || address.addressName
|
||||
form.destination = address.addressName
|
||||
form.destinationCountryCode = address.countryCode
|
||||
form.destinationWarehouseId = ''
|
||||
form.destinationWarehouseName = address.destinationWarehouseName
|
||||
form.destinationWarehouseName = address.addressName
|
||||
}
|
||||
|
||||
function savePrivateAddress() {
|
||||
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('请先完整填写私人地址和收件资料')
|
||||
function addressSnapshot(address: CustomerOrderAddress, role: FirstLegPartyRole): FirstLegPartySnapshot {
|
||||
return {
|
||||
id: '',
|
||||
code: '',
|
||||
name: address.companyName || address.addressName,
|
||||
role,
|
||||
contactName: address.contactName,
|
||||
phone: address.phone,
|
||||
email: address.email ?? '',
|
||||
countryRegion: address.countryCode,
|
||||
province: address.province ?? '',
|
||||
city: address.city,
|
||||
postalCode: address.postalCode ?? '',
|
||||
address: address.addressLine,
|
||||
}
|
||||
}
|
||||
|
||||
async function loadOrderAddresses() {
|
||||
orderAddressesLoading.value = true
|
||||
try {
|
||||
orderAddresses.value = await getMerchantOrderAddresses()
|
||||
} finally {
|
||||
orderAddressesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function selectPickupAddress(id: string) {
|
||||
const address = pickupAddresses.value.find((item) => String(item.id) === id)
|
||||
if (!address) return
|
||||
form.senderAddressId = String(address.id)
|
||||
form.shipperPartyId = ''
|
||||
form.shipperPartySnapshot = addressSnapshot(address, 'SHIPPER')
|
||||
form.shipper = address.companyName || address.addressName
|
||||
form.origin = address.city
|
||||
}
|
||||
|
||||
function addressPayload(addressType: CustomerOrderAddress['addressType'], snapshot: FirstLegPartySnapshot, addressName: string): CustomerOrderAddressSaveRequest {
|
||||
const payload: CustomerOrderAddressSaveRequest = {
|
||||
addressType,
|
||||
addressName: addressName.trim(),
|
||||
companyName: snapshot.name.trim() || addressName.trim(),
|
||||
contactName: snapshot.contactName.trim(),
|
||||
phone: snapshot.phone.trim(),
|
||||
countryCode: snapshot.countryRegion.trim().toUpperCase(),
|
||||
city: snapshot.city.trim(),
|
||||
addressLine: snapshot.address.trim(),
|
||||
defaultAddress: false,
|
||||
}
|
||||
if (snapshot.email.trim()) payload.email = snapshot.email.trim()
|
||||
if (snapshot.province.trim()) payload.province = snapshot.province.trim()
|
||||
if (snapshot.postalCode.trim()) payload.postalCode = snapshot.postalCode.trim()
|
||||
return payload
|
||||
}
|
||||
|
||||
function addressComplete(snapshot: FirstLegPartySnapshot | null, addressName: string) {
|
||||
return Boolean(snapshot && addressName.trim() && snapshot.contactName.trim() && snapshot.phone.trim()
|
||||
&& /^[A-Za-z]{2}$/.test(snapshot.countryRegion.trim()) && snapshot.city.trim() && snapshot.address.trim())
|
||||
}
|
||||
|
||||
async function savePickupAddress() {
|
||||
const snapshot = form.shipperPartySnapshot
|
||||
const addressName = snapshot?.name.trim() || `${snapshot?.city.trim() || '国内'}提货地址`
|
||||
if (!addressComplete(snapshot, addressName)) {
|
||||
ElMessage.warning('请先完整填写提货地址和联系人资料')
|
||||
return
|
||||
}
|
||||
const snapshot = { ...form.consigneePartySnapshot }
|
||||
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: partyId,
|
||||
consigneePartySnapshot: snapshot,
|
||||
consignee: snapshot.name,
|
||||
destination: form.destination,
|
||||
destinationCountryCode: form.destinationCountryCode.trim().toUpperCase(),
|
||||
destinationWarehouseName: form.destinationWarehouseName.trim(),
|
||||
try {
|
||||
const payload = addressPayload('PICKUP', snapshot!, addressName)
|
||||
if (form.senderAddressId) await updateMerchantOrderAddress(form.senderAddressId, payload)
|
||||
else form.senderAddressId = String(await createMerchantOrderAddress(payload))
|
||||
await loadOrderAddresses()
|
||||
selectPickupAddress(form.senderAddressId)
|
||||
ElMessage.success('企业提货地址已保存')
|
||||
} catch (error) {
|
||||
ElMessage.error(errorMessage(error))
|
||||
}
|
||||
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('已从当前账号的私人地址中移除')
|
||||
async function savePrivateAddress() {
|
||||
const snapshot = form.consigneePartySnapshot
|
||||
const addressName = form.destinationWarehouseName.trim()
|
||||
if (snapshot) snapshot.countryRegion = form.destinationCountryCode.trim().toUpperCase()
|
||||
if (!addressComplete(snapshot, addressName)) {
|
||||
ElMessage.warning('请先完整填写私人仓地址和收件资料')
|
||||
return
|
||||
}
|
||||
snapshot!.name = addressName
|
||||
try {
|
||||
const payload = addressPayload('PRIVATE_DESTINATION', snapshot!, addressName)
|
||||
if (form.privateDestinationAddressId) await updateMerchantOrderAddress(form.privateDestinationAddressId, payload)
|
||||
else form.privateDestinationAddressId = String(await createMerchantOrderAddress(payload))
|
||||
await loadOrderAddresses()
|
||||
selectPrivateAddress(form.privateDestinationAddressId)
|
||||
ElMessage.success('企业私人仓地址已保存')
|
||||
} catch (error) {
|
||||
ElMessage.error(errorMessage(error))
|
||||
}
|
||||
}
|
||||
|
||||
async function removeOrderAddress(type: CustomerOrderAddress['addressType']) {
|
||||
const id = type === 'PICKUP' ? form.senderAddressId : form.privateDestinationAddressId
|
||||
if (!id) return
|
||||
try {
|
||||
await disableMerchantOrderAddress(id)
|
||||
if (type === 'PICKUP') form.senderAddressId = ''
|
||||
else form.privateDestinationAddressId = ''
|
||||
await loadOrderAddresses()
|
||||
ElMessage.success('地址已从企业地址簿停用')
|
||||
} catch (error) {
|
||||
ElMessage.error(errorMessage(error))
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSelectedWarehouse(id: string | number | boolean | undefined) {
|
||||
@@ -1073,7 +1140,7 @@ async function openEdit(order: FirstLegOrder) {
|
||||
if (openingEditor.value || !canEditOrder.value || !canClientEditFirstLegOrder(order.status)) return
|
||||
openingEditor.value = true
|
||||
try {
|
||||
const detailedOrder = await store.loadDetail(order.id)
|
||||
const [detailedOrder] = await Promise.all([store.loadDetail(order.id), loadOrderAddresses()])
|
||||
const draft = draftFromOrder(detailedOrder)
|
||||
if (draft.destinationType !== 'PRIVATE') await loadDestinationWarehouses(draft.destinationType)
|
||||
const warehouseAvailable = availableWarehouses.value.some((item) => item.id === detailedOrder.receivingWarehouseId)
|
||||
@@ -1395,68 +1462,63 @@ function removeSelectedCargoItems() {
|
||||
resetEditorValidation()
|
||||
}
|
||||
|
||||
function downloadCargoTemplate() {
|
||||
const headers = cargoTemplateColumns.map(([label]) => `<th>${label}</th>`).join('')
|
||||
const example = cargoTemplateColumns.map(([label]) => {
|
||||
if (label === '包装类型') return '<td>CARTON</td>'
|
||||
if (label === '危险品') return '<td>否</td>'
|
||||
if (['每箱数量', '箱数', '总数量'].includes(label)) return '<td>1</td>'
|
||||
return '<td></td>'
|
||||
}).join('')
|
||||
const html = `<!doctype html><html><head><meta charset="UTF-8"></head><body><table><thead><tr>${headers}</tr></thead><tbody><tr>${example}</tr></tbody></table></body></html>`
|
||||
const url = URL.createObjectURL(new Blob(['\ufeff', html], { type: 'application/vnd.ms-excel;charset=utf-8' }))
|
||||
const anchor = document.createElement('a')
|
||||
anchor.href = url
|
||||
anchor.download = '货物明细导入模板.xls'
|
||||
anchor.click()
|
||||
URL.revokeObjectURL(url)
|
||||
function openBatchImport() {
|
||||
batchImportMode.value = 'PARTIAL'
|
||||
batchImportFile.value = null
|
||||
batchImportIdempotencyKey.value = ''
|
||||
batchImportResult.value = null
|
||||
batchImportOpen.value = true
|
||||
}
|
||||
|
||||
function parseCargoTemplateRows(text: string) {
|
||||
const documentNode = new DOMParser().parseFromString(text, 'text/html')
|
||||
const tableRows = [...documentNode.querySelectorAll('table tr')]
|
||||
if (tableRows.length < 2) return []
|
||||
const headers = [...tableRows[0].querySelectorAll('th,td')].map((cell) => cell.textContent?.trim() || '')
|
||||
return tableRows.slice(1).map((row) => {
|
||||
const values = [...row.querySelectorAll('td')].map((cell) => cell.textContent?.trim() || '')
|
||||
return { headers, values }
|
||||
}).filter(({ values }) => values.some(Boolean))
|
||||
}
|
||||
|
||||
async function importCargoTemplate(event: Event) {
|
||||
function selectBatchImportFile(event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
const file = input.files?.[0]
|
||||
input.value = ''
|
||||
if (!file) return
|
||||
if (!file.name.toLowerCase().endsWith('.xls')) {
|
||||
ElMessage.error('请上传由“下载模板”生成的 .xls 文件')
|
||||
if (!file.name.toLowerCase().endsWith('.xlsx')) {
|
||||
ElMessage.error('仅支持 .xlsx 格式的订单导入文件')
|
||||
return
|
||||
}
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
ElMessage.error('订单导入文件不能超过 5MB')
|
||||
return
|
||||
}
|
||||
batchImportFile.value = file
|
||||
batchImportIdempotencyKey.value = createIdempotencyKey('first-leg-batch-import')
|
||||
batchImportResult.value = null
|
||||
}
|
||||
|
||||
async function downloadBatchTemplate() {
|
||||
try {
|
||||
batchTemplateDownloading.value = true
|
||||
saveBlob(await downloadMerchantFirstLegBatchImportTemplate(), '头程订单批量导入模板.xlsx')
|
||||
} catch (error) {
|
||||
ElMessage.error(errorMessage(error))
|
||||
} finally {
|
||||
batchTemplateDownloading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function submitBatchImport() {
|
||||
if (!batchImportFile.value) {
|
||||
ElMessage.warning('请先选择 .xlsx 订单导入文件')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const parsedRows = parseCargoTemplateRows(await file.text())
|
||||
if (!parsedRows.length) throw new Error('模板中没有可导入的货物数据')
|
||||
const imported = parsedRows.map(({ headers, values }) => {
|
||||
const item = createEmptyCargoItem(`imported-cargo-${++cargoSequence}`)
|
||||
for (const [columnLabel, itemKey] of cargoTemplateColumns) {
|
||||
const value = values[headers.indexOf(columnLabel)] ?? ''
|
||||
if (itemKey === 'dangerousGoods') item.dangerousGoods = ['是', 'true', '1', 'yes'].includes(value.toLowerCase())
|
||||
else if (cargoNumberKeys.has(itemKey)) (item as unknown as Record<string, number>)[itemKey] = Number(value) || 0
|
||||
else (item as unknown as Record<string, string>)[itemKey] = value
|
||||
}
|
||||
item.packageType = item.packageType || 'CARTON'
|
||||
item.unitsPerPackage = item.unitsPerPackage || 1
|
||||
item.packageCount = item.packageCount || 1
|
||||
item.pieces = item.pieces || 1
|
||||
return item
|
||||
})
|
||||
const shouldReplaceDefaultRow = form.items.length === 1 && !form.items[0].cargoNameCn && !form.items[0].sku
|
||||
if (shouldReplaceDefaultRow) form.items.splice(0, 1, ...imported)
|
||||
else form.items.push(...imported)
|
||||
selectedCargoIds.value = []
|
||||
resetEditorValidation()
|
||||
ElMessage.success(`已导入 ${imported.length} 行货物明细`)
|
||||
batchImportSubmitting.value = true
|
||||
batchImportResult.value = await importMerchantFirstLegOrders(
|
||||
batchImportFile.value,
|
||||
batchImportMode.value,
|
||||
batchImportIdempotencyKey.value,
|
||||
)
|
||||
const result = batchImportResult.value.result
|
||||
if (result.succeeded) await store.load({ includeDrafts: true })
|
||||
if (result.failed) ElMessage.warning(`导入完成:成功 ${result.succeeded} 条,失败 ${result.failed} 条`)
|
||||
else ElMessage.success(`已成功导入 ${result.succeeded} 条订单草稿`)
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '货物明细导入失败')
|
||||
ElMessage.error(errorMessage(error))
|
||||
} finally {
|
||||
batchImportSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1634,6 +1696,7 @@ async function searchOrders(model: QueryModel) {
|
||||
<el-button link class="column-settings-reset" @click="resetOrderColumns"><RotateCcw :size="17" />恢复默认</el-button>
|
||||
</div>
|
||||
</el-popover>
|
||||
<el-button v-if="auth.hasPermission('order:first-leg:create')" @click="openBatchImport"><Upload :size="17" />批量导入订单</el-button>
|
||||
<el-button v-if="auth.hasPermission('order:first-leg:create')" type="primary" :loading="pageDataLoading" :disabled="pageDataLoading || availableWarehouses.length === 0" @click="openCreate"><FilePlus2 :size="17" />创建订单</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1751,6 +1814,9 @@ async function searchOrders(model: QueryModel) {
|
||||
<div class="prototype-form-flow shipping-form-flow">
|
||||
<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="senderAddressId" required :rules="requiredSelectRules('请选择企业提货地址')" class="prototype-form-row prototype-form-row--wide address-book-row">
|
||||
<div class="address-book-control"><el-select v-model="form.senderAddressId" clearable filterable :loading="orderAddressesLoading" placeholder="选择企业保存的提货地址" @change="selectPickupAddress"><el-option v-for="address in pickupAddresses" :key="String(address.id)" :label="addressLabel(address)" :value="String(address.id)" /></el-select><el-button @click="savePickupAddress">保存当前地址</el-button><el-button v-if="form.senderAddressId" link type="danger" @click="removeOrderAddress('PICKUP')">停用</el-button></div>
|
||||
</el-form-item>
|
||||
<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>
|
||||
@@ -1775,7 +1841,7 @@ async function searchOrders(model: QueryModel) {
|
||||
</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 value="FCL">整柜</el-radio><el-radio value="LCL">散货</el-radio></el-radio-group></el-form-item>
|
||||
<el-form-item label="是否商检" prop="customsInspection" required><el-radio-group v-model="form.customsInspection"><el-radio :value="true">是</el-radio><el-radio :value="false">否</el-radio></el-radio-group></el-form-item>
|
||||
<el-form-item label="是否商检" prop="customsInspection"><el-select v-model="form.customsInspection" clearable placeholder="暂不选择"><el-option label="是" :value="true" /><el-option label="否" :value="false" /></el-select><small v-if="form.items.some((item) => item.woodenProduct) && form.customsInspection !== true" class="field-guidance field-guidance--error">货物包含木制品,提交前必须选择“是”。</small></el-form-item>
|
||||
<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" type="textarea" :rows="3" resize="vertical" maxlength="300" show-word-limit placeholder="选填,例如:入仓前请电话联系" /></el-form-item>
|
||||
</div>
|
||||
@@ -1787,7 +1853,7 @@ async function searchOrders(model: QueryModel) {
|
||||
<el-form-item label="目的仓类型" prop="destinationType" required class="prototype-form-row prototype-form-row--tabs destination-type-row"><el-radio-group v-model="form.destinationType" class="segmented-options destination-type-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>
|
||||
<div class="address-book-control"><el-select v-model="form.privateDestinationAddressId" clearable filterable :loading="orderAddressesLoading" placeholder="选择企业保存的私人仓地址" @change="selectPrivateAddress"><el-option v-for="address in privateAddresses" :key="String(address.id)" :label="addressLabel(address)" :value="String(address.id)" /></el-select><el-button @click="savePrivateAddress">保存当前地址</el-button><el-button v-if="form.privateDestinationAddressId" link type="danger" @click="removeOrderAddress('PRIVATE_DESTINATION')">停用</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 :loading="destinationWarehousesLoading" 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="!destinationWarehousesLoading && availableDestinationWarehouses.length === 0" class="field-guidance">平台暂未维护可用的{{ form.destinationType === 'FBA' ? ' FBA 仓' : '海外仓' }},请联系平台管理员。</small></el-form-item>
|
||||
@@ -1808,7 +1874,7 @@ async function searchOrders(model: QueryModel) {
|
||||
<section class="order-form-section prototype-section cargo-section">
|
||||
<div class="order-section-heading cargo-table-heading"><div><h3>货物明细</h3></div></div>
|
||||
<div class="cargo-table-toolbar">
|
||||
<div class="cargo-import-actions"><input ref="cargoImportInput" class="cargo-import-input" type="file" accept=".xls,application/vnd.ms-excel" @change="importCargoTemplate" /><el-button class="cargo-upload-button" @click="cargoImportInput?.click()"><Upload :size="16" />上传 Excel</el-button><el-button class="cargo-download-button" @click="downloadCargoTemplate"><Download :size="16" />下载模板</el-button></div>
|
||||
<span class="cargo-table-hint">可逐行维护货物明细;批量订单请返回订单列表使用正式 Excel 导入。</span>
|
||||
<div class="cargo-table-actions"><el-button @click="addCargoItem"><Plus :size="16" />添加行</el-button><el-button type="danger" plain :disabled="!selectedCargoIds.length" @click="removeSelectedCargoItems"><Trash2 :size="15" />删除所选行</el-button></div>
|
||||
</div>
|
||||
<div class="cargo-table-wrap">
|
||||
@@ -1817,7 +1883,7 @@ async function searchOrders(model: QueryModel) {
|
||||
<th class="cargo-check-cell"><el-checkbox :model-value="allCargoSelected" :indeterminate="someCargoSelected" aria-label="全选货物" @change="toggleAllCargoItems" /></th><th class="cargo-index-cell">#</th>
|
||||
<th>SKU NO.<small>SKU必填</small></th><th>中文品名</th><th class="cargo-image-column">图片</th><th>英文品名</th><th>报关 HS CODE</th><th>清关 HS CODE</th><th>包装类型</th>
|
||||
<th class="cargo-number-column">箱规长 cm</th><th class="cargo-number-column">箱规宽 cm</th><th class="cargo-number-column">箱规高 cm</th><th class="cargo-number-column">每箱数量</th><th class="cargo-number-column">箱数</th><th class="cargo-number-column">总毛重</th><th class="cargo-number-column">总体积</th><th class="cargo-number-column">总数量</th><th class="cargo-number-column">总净重</th>
|
||||
<th>清关申报单价 USD</th><th>清关申报金额 USD</th><th>材质</th><th>用途</th><th>型号</th><th>品牌</th><th class="cargo-dimensions-column">产品规格 长×宽×高</th><th>危险品</th><th>货件编号<small>货件编号必填</small></th><th>跟踪编号<small>跟踪编号必填</small></th><th>UPC NO.</th><th>备注</th><th class="cargo-operation-column">操作</th>
|
||||
<th>清关申报单价 USD</th><th>清关申报金额 USD</th><th>材质</th><th>用途</th><th>型号</th><th>品牌</th><th class="cargo-dimensions-column">产品规格 长×宽×高</th><th>危险品</th><th>木制品</th><th>货件编号<small>货件编号必填</small></th><th>跟踪编号<small>跟踪编号必填</small></th><th>UPC NO.</th><th>备注</th><th class="cargo-operation-column">操作</th>
|
||||
</tr></thead>
|
||||
<tbody><tr v-for="(item, index) in form.items" :key="item.id">
|
||||
<td class="cargo-check-cell"><el-checkbox :model-value="selectedCargoIds.includes(item.id)" :aria-label="`选择货物 ${index + 1}`" @change="toggleCargoItem(item.id, $event)" /></td><td class="cargo-index-cell">{{ index + 1 }}</td>
|
||||
@@ -1829,7 +1895,7 @@ async function searchOrders(model: QueryModel) {
|
||||
<td><el-input-number v-model="item.lengthCm" :min="0" :precision="2" :controls="false" /></td><td><el-input-number v-model="item.widthCm" :min="0" :precision="2" :controls="false" /></td><td><el-input-number v-model="item.heightCm" :min="0" :precision="2" :controls="false" /></td>
|
||||
<td><el-form-item :prop="`items.${index}.unitsPerPackage`" required :rules="positiveNumberRules('每箱数量必须大于 0', true)"><el-input-number v-model="item.unitsPerPackage" :min="1" :controls="false" /></el-form-item></td><td><el-form-item :prop="`items.${index}.packageCount`" required :rules="positiveNumberRules('箱数必须大于 0', true)"><el-input-number v-model="item.packageCount" :min="1" :controls="false" /></el-form-item></td><td><el-form-item :prop="`items.${index}.grossWeightKg`" required :rules="positiveNumberRules('总毛重必须大于 0')"><el-input-number v-model="item.grossWeightKg" :min="0" :precision="3" :controls="false" /></el-form-item></td><td><el-form-item :prop="`items.${index}.volumeCbm`" required :rules="positiveNumberRules('总体积必须大于 0')"><el-input-number v-model="item.volumeCbm" :min="0" :precision="4" :controls="false" /></el-form-item></td><td><el-form-item :prop="`items.${index}.pieces`" required :rules="positiveNumberRules('总数量必须大于 0', true)"><el-input-number v-model="item.pieces" :min="1" :controls="false" /></el-form-item></td><td><el-input-number v-model="item.netWeightKg" :min="0" :precision="3" :controls="false" /></td>
|
||||
<td><el-input-number v-model="item.unitDeclaredValue" :min="0" :precision="2" :controls="false" /></td><td><el-input-number v-model="item.declaredValue" :min="0" :precision="2" :controls="false" /></td><td><el-input v-model="item.material" maxlength="128" placeholder="材质" /></td><td><el-input v-model="item.usage" maxlength="128" placeholder="用途" /></td><td><el-input v-model="item.model" maxlength="64" placeholder="型号" /></td><td><el-input v-model="item.brand" maxlength="64" placeholder="品牌" /></td>
|
||||
<td class="cargo-dimensions-column"><div class="cargo-table-dimensions"><el-input-number v-model="item.productLengthCm" :min="0" :precision="2" :controls="false" /><span>×</span><el-input-number v-model="item.productWidthCm" :min="0" :precision="2" :controls="false" /><span>×</span><el-input-number v-model="item.productHeightCm" :min="0" :precision="2" :controls="false" /></div></td><td class="cargo-danger-cell"><el-switch v-model="item.dangerousGoods" /></td>
|
||||
<td class="cargo-dimensions-column"><div class="cargo-table-dimensions"><el-input-number v-model="item.productLengthCm" :min="0" :precision="2" :controls="false" /><span>×</span><el-input-number v-model="item.productWidthCm" :min="0" :precision="2" :controls="false" /><span>×</span><el-input-number v-model="item.productHeightCm" :min="0" :precision="2" :controls="false" /></div></td><td class="cargo-danger-cell"><el-switch v-model="item.dangerousGoods" /></td><td class="cargo-danger-cell"><el-switch v-model="item.woodenProduct" /></td>
|
||||
<td><el-form-item :prop="`items.${index}.shipmentId`" :required="form.destinationType === 'FBA'" :rules="form.destinationType === 'FBA' ? requiredTextRules('FBA 货物请输入货件编号') : undefined"><el-input v-model="item.shipmentId" maxlength="64" placeholder="货件编号" /></el-form-item></td><td><el-form-item :prop="`items.${index}.referenceId`" :required="form.destinationType === 'FBA'" :rules="form.destinationType === 'FBA' ? requiredTextRules('FBA 货物请输入跟踪编号') : undefined"><el-input v-model="item.referenceId" maxlength="64" placeholder="跟踪编号" /></el-form-item></td><td><el-form-item :prop="`items.${index}.upc`"><el-input v-model="item.upc" maxlength="64" placeholder="UPC NO.(选填)" /></el-form-item></td><td><el-input v-model="item.remark" maxlength="500" placeholder="备注" /></td><td class="cargo-operation-column"><el-button link type="danger" :aria-label="`删除货物 ${index + 1}`" @click="removeCargoItem(index)"><Trash2 :size="15" />删除</el-button></td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
@@ -1875,6 +1941,32 @@ async function searchOrders(model: QueryModel) {
|
||||
<template #footer><div class="editor-actions"><el-button :disabled="saving" @click="closeEditor">返回</el-button><el-button v-if="editingId ? canEditOrder : canCreateOrder" :loading="saving" @click="saveDraft">保存草稿</el-button><el-button v-if="auth.hasPermission('order:first-leg:submit')" type="primary" :loading="saving" @click="submitOrder"><Send :size="16" />{{ companySubmissionBlocked ? '提交前需完成资料审核' : '提交订单' }}</el-button></div></template>
|
||||
</FirstLegOrderEditorShell>
|
||||
|
||||
<el-dialog v-model="batchImportOpen" title="批量导入头程订单" width="680px" destroy-on-close append-to-body>
|
||||
<div class="batch-import-dialog">
|
||||
<el-alert title="使用服务端正式模板导入" description="仅支持 .xlsx,文件不超过 5MB、最多 50 行。导入结果按本企业权限和数据范围返回。" type="info" :closable="false" show-icon />
|
||||
<div class="batch-import-actions">
|
||||
<input ref="batchImportInput" class="cargo-import-input" type="file" accept=".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" @change="selectBatchImportFile" />
|
||||
<el-button :loading="batchTemplateDownloading" @click="downloadBatchTemplate"><Download :size="16" />下载正式模板</el-button>
|
||||
<el-button type="primary" plain @click="batchImportInput?.click()"><Upload :size="16" />选择 Excel</el-button>
|
||||
<span>{{ batchImportFile?.name || '尚未选择文件' }}</span>
|
||||
</div>
|
||||
<el-radio-group v-model="batchImportMode" aria-label="导入模式">
|
||||
<el-radio value="PARTIAL">部分成功:有效行创建草稿,失败行返回原因</el-radio>
|
||||
<el-radio value="ATOMIC">原子导入:任一行失败则整批不创建</el-radio>
|
||||
</el-radio-group>
|
||||
<section v-if="batchImportResult" class="batch-import-result" aria-live="polite">
|
||||
<strong>批次 {{ batchImportResult.batchNo }}</strong>
|
||||
<span>共 {{ batchImportResult.result.total }} 条,成功 {{ batchImportResult.result.succeeded }} 条,失败 {{ batchImportResult.result.failed }} 条</span>
|
||||
<ul v-if="batchImportResult.result.failed">
|
||||
<li v-for="item in batchImportResult.result.items.filter((row) => !row.success)" :key="`${item.index}-${item.clientRowId}`">
|
||||
第 {{ item.index + 1 }} 行({{ item.clientRowId || '未填写行标识' }}):{{ item.errorMessage || item.errorCode || '导入失败' }}
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
<template #footer><div class="editor-actions"><el-button :disabled="batchImportSubmitting" @click="batchImportOpen = false">关闭</el-button><el-button type="primary" :loading="batchImportSubmitting" :disabled="!batchImportFile" @click="submitBatchImport">开始导入</el-button></div></template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="detailOpen" class="first-leg-detail-dialog" title="订单详情" width="94vw" top="3vh" destroy-on-close append-to-body>
|
||||
<div v-loading="detailLoading" class="order-detail-content">
|
||||
<div v-if="detailError" class="order-load-state is-compact" role="alert">
|
||||
@@ -1904,7 +1996,7 @@ async function searchOrders(model: QueryModel) {
|
||||
<div><dt>目的国家 / 仓库</dt><dd>{{ detailOrder.destinationCountryCode || '-' }} · {{ detailOrder.destinationWarehouseName || '-' }}</dd></div>
|
||||
<div v-if="detailOrder.destinationType === 'FBA'"><dt>Amazon 仓库代码</dt><dd>{{ displayValue(detailOrder.amazonWarehouseCode) }}</dd></div>
|
||||
<div><dt>期望出运</dt><dd>{{ displayValue(detailOrder.expectedDepartureDate) }}</dd></div>
|
||||
<div><dt>海关查验</dt><dd>{{ detailOrder.customsInspection ? '需要' : '不需要' }}</dd></div>
|
||||
<div><dt>海关查验</dt><dd>{{ detailOrder.customsInspection == null ? '未选择' : (detailOrder.customsInspection ? '需要' : '不需要') }}</dd></div>
|
||||
<div><dt>提交时间</dt><dd>{{ detailOrder.submittedAt ? formatDateTime(detailOrder.submittedAt) : '-' }}</dd></div>
|
||||
<div><dt>完成时间</dt><dd>{{ detailOrder.completedAt ? formatDateTime(detailOrder.completedAt) : '-' }}</dd></div>
|
||||
</dl>
|
||||
@@ -1973,6 +2065,7 @@ async function searchOrders(model: QueryModel) {
|
||||
<el-table-column label="品牌" width="110" show-overflow-tooltip><template #default="{ row }">{{ row.brand || '-' }}</template></el-table-column>
|
||||
<el-table-column label="产品规格 长×宽×高(cm)" width="180" align="center"><template #default="{ row }">{{ row.productLengthCm || '-' }} × {{ row.productWidthCm || '-' }} × {{ row.productHeightCm || '-' }}</template></el-table-column>
|
||||
<el-table-column label="危险品" width="82" align="center"><template #default="{ row }">{{ row.dangerousGoods ? '是' : '否' }}</template></el-table-column>
|
||||
<el-table-column label="木制品" width="82" align="center"><template #default="{ row }">{{ row.woodenProduct ? '是' : '否' }}</template></el-table-column>
|
||||
<el-table-column label="备注" min-width="160" show-overflow-tooltip><template #default="{ row }">{{ row.remark || '-' }}</template></el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
@@ -2043,6 +2136,13 @@ async function searchOrders(model: QueryModel) {
|
||||
.order-table-toolbar span { color: var(--muted); font-size: 11px; }
|
||||
.order-table-toolbar > .order-table-toolbar-actions { align-items: center; gap: 8px; }
|
||||
.order-table-toolbar-actions :deep(.el-button + .el-button) { margin-left: 0; }
|
||||
.batch-import-dialog { display: flex; flex-direction: column; gap: 18px; }
|
||||
.batch-import-actions { display: flex; min-width: 0; align-items: center; flex-wrap: wrap; gap: 10px; }
|
||||
.batch-import-actions > span { min-width: 0; overflow: hidden; color: var(--muted); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.batch-import-dialog :deep(.el-radio-group) { display: flex; align-items: flex-start; flex-direction: column; gap: 8px; }
|
||||
.batch-import-result { display: flex; flex-direction: column; gap: 6px; padding: 14px; border: 1px solid var(--border); background: #f8fafc; }
|
||||
.batch-import-result > span, .batch-import-result li { color: var(--muted); font-size: 12px; line-height: 1.55; }
|
||||
.batch-import-result ul { max-height: 180px; margin: 4px 0 0; padding-left: 20px; overflow-y: auto; }
|
||||
.column-settings-trigger { width: 40px; min-width: 40px; height: 36px; padding: 0; color: #53627b; }
|
||||
:global(.order-column-popper.el-popover) { padding: 0; border-color: #e3e8f0; border-radius: 4px; box-shadow: 0 8px 24px rgba(29, 45, 74, .12); }
|
||||
:global(.order-column-popper .column-settings-panel) { padding: 14px 10px 10px; }
|
||||
|
||||
@@ -183,9 +183,11 @@ function buildOrderDraft(row: QuickOrderRow, channel: QuickChannelOption): First
|
||||
receivingWarehouseId: channel.warehouseId,
|
||||
expectedDepartureDate: '',
|
||||
shipperPartyId: '',
|
||||
senderAddressId: '',
|
||||
shipperPartySnapshot: null,
|
||||
shipper: '',
|
||||
consigneePartyId: '',
|
||||
privateDestinationAddressId: '',
|
||||
consigneePartySnapshot: row.destinationType === 'PRIVATE' && hasCompletePrivateContact(row)
|
||||
? createConsigneeSnapshot(row) : null,
|
||||
consignee: row.destinationType === 'PRIVATE' && hasCompletePrivateContact(row)
|
||||
@@ -201,7 +203,7 @@ function buildOrderDraft(row: QuickOrderRow, channel: QuickChannelOption): First
|
||||
channelName: channel.channelName,
|
||||
pricingTaxMode,
|
||||
taxMode: 'BUY_DECLARATION',
|
||||
customsInspection: false,
|
||||
customsInspection: null,
|
||||
destinationType: row.destinationType,
|
||||
amazonWarehouseCode: row.amazonWarehouseCode,
|
||||
shipmentId: '',
|
||||
|
||||
Reference in New Issue
Block a user