623 lines
32 KiB
TypeScript
623 lines
32 KiB
TypeScript
import { computed, ref } from 'vue'
|
|
import { defineStore } from 'pinia'
|
|
import {
|
|
createMerchantFirstLegOrderBatch,
|
|
createMerchantFirstLegOrder,
|
|
createIdempotencyKey,
|
|
getMerchantFirstLegOrder,
|
|
getMerchantFirstLegOrders,
|
|
getMerchantWarehouseDetail,
|
|
getMerchantWarehouses,
|
|
submitMerchantFirstLegOrder,
|
|
updateMerchantFirstLegOrder,
|
|
type ApiFirstLegStatus,
|
|
type FirstLegOrderSaveRequest,
|
|
type MerchantFirstLegOrder,
|
|
type MerchantFirstLegOrderRow,
|
|
type MerchantFirstLegOrderDetail,
|
|
type MerchantBatchDraftResponse,
|
|
type MerchantWarehouse,
|
|
} from '@/modules/first-leg/api/firstLeg'
|
|
import {
|
|
getMerchantUnifiedOrder,
|
|
getMerchantUnifiedOrders,
|
|
isUnifiedOrderQueryUnavailable,
|
|
type MerchantUnifiedOrderDetail,
|
|
type MerchantUnifiedOrderQuery,
|
|
type MerchantUnifiedOrderRow,
|
|
} from '@/modules/first-leg/api/unifiedOrders'
|
|
import type { MerchantTenantStatus } from '@/modules/tenant-management/public'
|
|
import type { PlatformReceivingWarehouse, PlatformReceivingWarehouseSnapshot } from '@chongchong/shared'
|
|
import type { FirstLegPartySnapshot } from '@/modules/first-leg/data/firstLegParties'
|
|
import type { FirstLegOrder, FirstLegOrderDraft, FirstLegOrderTimelineItem } from '@/modules/first-leg/types/firstLegOrder'
|
|
import { canClientEditFirstLegOrder, getFirstLegStatusLabel } from '@/modules/first-leg/utils/firstLegOrder'
|
|
import { loadAllWarehouseRecords } from '@/modules/first-leg/utils/warehousePagination'
|
|
|
|
export type ClientWarehouseType = 'DOMESTIC' | 'FBA' | 'OVERSEAS'
|
|
export type ClientWarehouse = PlatformReceivingWarehouse & {
|
|
warehouseType: ClientWarehouseType
|
|
countryCode: string
|
|
}
|
|
|
|
function clientStatus(status: ApiFirstLegStatus): FirstLegOrder['status'] {
|
|
if (status === 'DRAFT') return 'DRAFT'
|
|
if (status === 'SUBMITTED') return 'PENDING_REVIEW'
|
|
// 后端联调任务曾出现 REJECTED,但一期已确认订单不设驳回状态;兼容旧数据时仍按待审核展示。
|
|
if (status === 'REJECTED') return 'PENDING_REVIEW'
|
|
if (status === 'APPROVED' || status === 'ACCEPTED' || status === 'PROCESSING') return 'WAIT_MEASURE'
|
|
if (status === 'COMPLETED') return 'LOADED'
|
|
if (status === 'CANCELLED') return 'PENDING_REVIEW'
|
|
return 'PENDING_REVIEW'
|
|
}
|
|
|
|
function clientTransport(order: MerchantFirstLegOrder): FirstLegOrder['transportMode'] {
|
|
if (order.transportMode === 'SEA') return order.containerMode === 'FULL_CONTAINER' ? 'FCL' : 'LCL'
|
|
if (order.transportMode === 'AIR') return 'AIR'
|
|
return 'ROAD'
|
|
}
|
|
|
|
function unifiedTransport(containerMode: MerchantUnifiedOrderRow['containerMode']): FirstLegOrder['transportMode'] {
|
|
if (containerMode === 'FULL_CONTAINER') return 'FCL'
|
|
if (containerMode === 'LCL') return 'LCL'
|
|
return null
|
|
}
|
|
|
|
function apiTransport(draft: FirstLegOrderDraft): FirstLegOrderSaveRequest['transportMode'] {
|
|
if (draft.transportMode === 'AIR') return 'AIR'
|
|
if (draft.transportMode === 'ROAD') return 'TRUCK'
|
|
return 'SEA'
|
|
}
|
|
|
|
function savePayload(draft: FirstLegOrderDraft, warehouse: ClientWarehouse, customerName: string): FirstLegOrderSaveRequest {
|
|
return {
|
|
inboundCode: draft.inboundCode?.trim() || undefined,
|
|
customerName: customerName.trim(),
|
|
fulfillmentType: draft.fulfillmentType ?? 'STANDARD',
|
|
lxCode: draft.fulfillmentType === 'ONE_CLICK_DROPSHIP' ? draft.lxCode?.trim() || undefined : undefined,
|
|
transportMode: apiTransport(draft),
|
|
tradeTerm: draft.tradeTerm || undefined,
|
|
destinationCountryCode: draft.destinationCountryCode.trim().toUpperCase(),
|
|
destinationWarehouseId: draft.destinationType === 'PRIVATE' ? undefined : draft.destinationWarehouseId,
|
|
destinationWarehouseName: draft.destinationWarehouseName,
|
|
expectedShipDate: draft.expectedDepartureDate || undefined,
|
|
customerRemark: draft.customerRemark || undefined,
|
|
senderAddressId: draft.pickupMethod === 'PICKUP' ? draft.senderAddressId : undefined,
|
|
privateDestinationAddressId: draft.destinationType === 'PRIVATE' ? draft.privateDestinationAddressId : undefined,
|
|
cargoes: draft.items.map((item) => ({
|
|
cargoName: item.cargoNameCn,
|
|
cargoNameEn: item.cargoNameEn || undefined,
|
|
hsCode: item.hsCode || undefined,
|
|
customsHsCode: item.customsHsCode || undefined,
|
|
packageType: item.packageType,
|
|
packageCount: item.packageCount,
|
|
packingGroupNo: draft.fulfillmentType === 'ONE_CLICK_DROPSHIP' ? item.packingGroupNo.trim() || undefined : undefined,
|
|
grossWeightKg: item.grossWeightKg,
|
|
volumeCbm: item.volumeCbm,
|
|
lengthCm: item.lengthCm || undefined,
|
|
widthCm: item.widthCm || undefined,
|
|
heightCm: item.heightCm || undefined,
|
|
declaredValue: item.declaredValue || undefined,
|
|
currency: item.currency || undefined,
|
|
dangerousGoods: item.dangerousGoods,
|
|
woodenProduct: item.woodenProduct,
|
|
cargoRemark: item.remark || undefined,
|
|
skuNo: item.sku || undefined,
|
|
upcNo: item.upc || undefined,
|
|
totalQuantity: item.pieces,
|
|
unitsPerPackage: item.unitsPerPackage || undefined,
|
|
netWeightKg: item.netWeightKg || undefined,
|
|
unitDeclaredValue: item.unitDeclaredValue || undefined,
|
|
material: item.material || undefined,
|
|
usage: item.usage || undefined,
|
|
model: item.model || undefined,
|
|
brand: item.brand || undefined,
|
|
productLengthCm: item.productLengthCm || undefined,
|
|
productWidthCm: item.productWidthCm || undefined,
|
|
productHeightCm: item.productHeightCm || undefined,
|
|
shipmentId: item.shipmentId || undefined,
|
|
referenceId: item.referenceId || undefined,
|
|
productImageFileId: item.productImageFileId || undefined,
|
|
})),
|
|
pickupMethod: draft.pickupMethod,
|
|
receivingWarehouseId: warehouse.id,
|
|
receivingWarehouseName: warehouse.name,
|
|
channelCode: draft.channelCode,
|
|
channelName: draft.channelName,
|
|
pricingTaxMode: draft.pricingTaxMode,
|
|
taxMode: draft.taxMode,
|
|
containerMode: draft.transportMode === 'FCL' ? 'FULL_CONTAINER' : draft.transportMode === 'LCL' ? 'LCL' : undefined,
|
|
customsInspection: draft.customsInspection,
|
|
destinationType: draft.destinationType,
|
|
amazonWarehouseCode: draft.destinationType === 'FBA' ? draft.amazonWarehouseCode || undefined : undefined,
|
|
shipmentId: draft.destinationType === 'FBA' ? draft.items.find((item) => item.shipmentId)?.shipmentId || undefined : undefined,
|
|
referenceId: draft.items.find((item) => item.referenceId)?.referenceId || draft.referenceNo || undefined,
|
|
}
|
|
}
|
|
|
|
function prepareDraftForSave(draft: FirstLegOrderDraft, warehouses: ClientWarehouse[]) {
|
|
const warehouse = warehouses.find((item) => item.id === draft.receivingWarehouseId)
|
|
if (!warehouse || warehouse.warehouseType !== 'DOMESTIC') throw new Error('所选国内收货仓当前不可用')
|
|
if (draft.destinationType !== 'PRIVATE') {
|
|
const destinationWarehouse = warehouses.find((item) => item.id === draft.destinationWarehouseId && item.warehouseType === draft.destinationType)
|
|
if (!destinationWarehouse) throw new Error('所选目的仓当前不可用,请重新选择')
|
|
draft.destinationWarehouseName = destinationWarehouse.name
|
|
draft.destinationCountryCode = destinationWarehouse.countryCode
|
|
draft.amazonWarehouseCode = draft.destinationType === 'FBA' ? destinationWarehouse.code : ''
|
|
} else {
|
|
draft.destinationWarehouseId = ''
|
|
}
|
|
return warehouse
|
|
}
|
|
|
|
function warehouseSnapshot(warehouse?: PlatformReceivingWarehouse): PlatformReceivingWarehouseSnapshot | null {
|
|
return warehouse ? { id: warehouse.id, code: warehouse.code, name: warehouse.name, province: warehouse.province, city: warehouse.city, address: warehouse.address } : null
|
|
}
|
|
|
|
function orderWarehouseSnapshot(order: MerchantFirstLegOrder, warehouse?: PlatformReceivingWarehouse): PlatformReceivingWarehouseSnapshot | null {
|
|
if (order.receivingWarehouseAddress) {
|
|
return {
|
|
id: String(order.receivingWarehouseId),
|
|
code: warehouse?.code ?? '',
|
|
name: order.receivingWarehouseName,
|
|
province: warehouse?.province ?? '',
|
|
city: warehouse?.city ?? '',
|
|
address: order.receivingWarehouseAddress,
|
|
}
|
|
}
|
|
return warehouseSnapshot(warehouse)
|
|
}
|
|
|
|
function partySnapshot(detail: MerchantFirstLegOrderDetail, type: 'SENDER' | 'RECEIVER'): FirstLegPartySnapshot | null {
|
|
const party = (detail.parties ?? []).find((item) => item.partyType === type)
|
|
if (!party) return null
|
|
return {
|
|
id: String(party.id), code: type, name: party.companyName, role: type === 'SENDER' ? 'SHIPPER' : 'CONSIGNEE',
|
|
contactName: party.contactName, phone: party.phone, email: party.email ?? '', countryRegion: party.countryCode,
|
|
province: party.province ?? '', city: party.city, postalCode: party.postalCode ?? '', address: party.addressLine,
|
|
}
|
|
}
|
|
|
|
function timeline(detail: MerchantFirstLegOrderDetail): FirstLegOrderTimelineItem[] {
|
|
return (detail.statusHistory ?? []).map((item) => {
|
|
const status = clientStatus(item.toStatus)
|
|
return {
|
|
id: String(item.id),
|
|
kind: 'MAIN_STATUS',
|
|
status: status === 'DRAFT' ? null : status,
|
|
nodeType: '',
|
|
title: item.toStatus === 'REJECTED' ? '订单信息已补充' : item.action || '订单进度已更新',
|
|
description: item.reason ?? '',
|
|
operatorName: item.operatorName ?? '',
|
|
occurredAt: item.occurredAt,
|
|
plannedAt: '',
|
|
completed: true,
|
|
attachmentCount: 0,
|
|
}
|
|
})
|
|
}
|
|
|
|
function businessSummary(summary?: MerchantUnifiedOrderRow): FirstLegOrder['businessSummary'] {
|
|
return {
|
|
totalPackageCount: summary?.totalPackageCount ?? null,
|
|
actualGrossWeightKg: summary?.actualGrossWeightKg ?? null,
|
|
actualVolumeCbm: summary?.actualVolumeCbm ?? null,
|
|
chargeableWeightKg: summary?.measurement?.chargeableWeightKg ?? null,
|
|
measurementCompletedAt: summary?.measurement?.completedAt ?? '',
|
|
preloadStatus: summary?.preload?.status ?? '',
|
|
preloadItemCount: summary?.preload?.itemCount ?? null,
|
|
loadingStatus: summary?.loading?.status ?? '',
|
|
loadingVersion: summary?.loading?.loadingVersion ?? null,
|
|
pickingVersion: summary?.loading?.pickingVersion ?? null,
|
|
expectedPickCount: summary?.loading?.expectedCount ?? null,
|
|
pickedCount: summary?.loading?.pickedCount ?? null,
|
|
evidenceCount: summary?.loading?.evidenceCount ?? null,
|
|
sealNo: summary?.loading?.sealNo ?? '',
|
|
deliveryStatus: summary?.delivery?.status ?? '',
|
|
deliveryCompletedAt: summary?.delivery?.completedAt ?? '',
|
|
deliveryTrackingNo: summary?.delivery?.trackingNo ?? '',
|
|
containerNo: summary?.transport?.containerNo ?? '',
|
|
cabinetNo: summary?.transport?.cabinetNo ?? '',
|
|
vesselName: summary?.transport?.vesselName ?? '',
|
|
voyageNo: summary?.transport?.voyageNo ?? '',
|
|
sailingAt: summary?.transport?.sailingAt ?? '',
|
|
portArrivalAt: summary?.transport?.portArrivalAt ?? '',
|
|
inboundId: summary?.inboundId == null ? '' : String(summary.inboundId),
|
|
inboundVersion: summary?.inboundVersion ?? null,
|
|
inboundCode: summary?.inboundCode ?? '',
|
|
nextAction: summary?.nextAction ?? '',
|
|
}
|
|
}
|
|
|
|
const unifiedActionLabels: Readonly<Record<string, string>> = {
|
|
SUBMIT_ORDER: '订单已提交',
|
|
APPROVE_ORDER: '订单审核通过',
|
|
CONFIRM_MEASUREMENT: '测量数据已确认',
|
|
PALLETIZE_DROPSHIP: '一件代发组托',
|
|
SMART_SPLIT: '分柜方案已更新',
|
|
CONFIRM_PRELOAD: '预装清单已确认',
|
|
COMPLETE_LOADING: '装柜已完成',
|
|
COMPLETE_DELIVERY: '派送已完成',
|
|
}
|
|
|
|
function unifiedTimeline(detail: MerchantUnifiedOrderDetail): FirstLegOrderTimelineItem[] {
|
|
const statusItems: FirstLegOrderTimelineItem[] = (detail.timeline ?? []).flatMap((item, index) => item.mainStatus ? [{
|
|
id: `status-${item.source}-${item.eventType}-${index}`,
|
|
kind: 'MAIN_STATUS' as const,
|
|
status: item.mainStatus,
|
|
nodeType: '',
|
|
title: (item.action && unifiedActionLabels[item.action]) || (item.mainStatus ? `进入${getFirstLegStatusLabel(item.mainStatus)}` : '订单进度已更新'),
|
|
description: item.remark ?? '',
|
|
operatorName: item.operatorName ?? '',
|
|
occurredAt: item.occurredAt ?? '',
|
|
plannedAt: '',
|
|
completed: true,
|
|
attachmentCount: 0,
|
|
}] : [])
|
|
const customItems: FirstLegOrderTimelineItem[] = (detail.customNodes ?? [])
|
|
.filter((item) => item.customerVisible)
|
|
.map((item) => ({
|
|
id: `node-${item.id}`,
|
|
kind: 'CUSTOM_NODE',
|
|
status: null,
|
|
nodeType: item.nodeType,
|
|
title: item.name,
|
|
description: item.remark ?? '',
|
|
operatorName: item.operatorName ?? '',
|
|
occurredAt: item.completedAt ?? item.plannedAt ?? '',
|
|
plannedAt: item.plannedAt ?? '',
|
|
completed: Boolean(item.completedAt),
|
|
attachmentCount: item.attachmentIds?.length ?? 0,
|
|
}))
|
|
return [...statusItems, ...customItems].sort((left, right) => {
|
|
const timeDifference = Date.parse(left.occurredAt) - Date.parse(right.occurredAt)
|
|
return Number.isNaN(timeDifference) ? 0 : timeDifference
|
|
})
|
|
}
|
|
|
|
function mapOrder(order: MerchantFirstLegOrder, warehouses: PlatformReceivingWarehouse[], detail?: MerchantFirstLegOrderDetail): FirstLegOrder {
|
|
const warehouse = warehouses.find((item) => item.id === String(order.receivingWarehouseId))
|
|
const shipper = detail ? partySnapshot(detail, 'SENDER') : null
|
|
const consignee = detail ? partySnapshot(detail, 'RECEIVER') : null
|
|
return {
|
|
id: String(order.id ?? order.orderId ?? ''), orderNo: order.orderNo, referenceNo: order.referenceId ?? '',
|
|
transportMode: order.transportMode ? clientTransport(order) : null,
|
|
businessType: 'EXPORT', fulfillmentType: order.fulfillmentType ?? 'STANDARD', lxCode: order.lxCode ?? '', origin: shipper?.city ?? '', destination: order.destinationWarehouseName || order.destinationCountryCode || '',
|
|
receivingWarehouseId: String(order.receivingWarehouseId), receivingWarehouseName: order.receivingWarehouseName,
|
|
receivingWarehouseSnapshot: orderWarehouseSnapshot(order, warehouse), expectedDepartureDate: order.expectedShipDate ?? '', etd: '', eta: '',
|
|
shipperPartyId: shipper?.id ?? '', shipperPartySnapshot: shipper, shipper: shipper?.name ?? '',
|
|
consigneePartyId: consignee?.id ?? '', consigneePartySnapshot: consignee, consignee: consignee?.name ?? '',
|
|
expressCompanyName: order.expressCompany ?? '', trackingNumber: order.expressTrackingNo ?? '', skuNos: order.skuNos ?? [],
|
|
totalNetWeightKg: order.totalNetWeightKg ?? null, totalQuantity: order.totalQuantity ?? null,
|
|
chargeableVolumeCbm: null, inboundedAt: order.inboundedAt ?? '', businessDate: '',
|
|
priority: 'NORMAL', customerRemark: order.customerRemark ?? '', operatorName: '',
|
|
status: clientStatus(order.status), version: order.version ?? 0,
|
|
items: (detail?.cargoes ?? []).map((item) => ({
|
|
id: String(item.id), sku: item.skuNo ?? '', upc: item.upcNo ?? '', cargoNameCn: item.cargoName, cargoNameEn: item.cargoNameEn ?? '',
|
|
productImageFileId: item.productImageFileId == null ? '' : String(item.productImageFileId),
|
|
productImageName: '', productImagePreviewUrl: '', productImageUploading: false, productImageError: '',
|
|
customsHsCode: item.customsHsCode ?? '', lengthCm: item.lengthCm ?? 0, widthCm: item.widthCm ?? 0,
|
|
heightCm: item.heightCm ?? 0, unitsPerPackage: item.unitsPerPackage ?? 1,
|
|
pieces: item.totalQuantity ?? item.packageCount, packageCount: item.packageCount, packingGroupNo: item.packingGroupNo ?? '', grossWeightKg: item.grossWeightKg,
|
|
volumeCbm: item.volumeCbm, remark: item.cargoRemark ?? '',
|
|
packageType: item.packageType, hsCode: item.hsCode ?? '', declaredValue: item.declaredValue ?? 0,
|
|
currency: item.currency ?? 'USD', dangerousGoods: item.dangerousGoods ?? false,
|
|
woodenProduct: item.woodenProduct ?? false,
|
|
netWeightKg: item.netWeightKg ?? 0, unitDeclaredValue: item.unitDeclaredValue ?? 0,
|
|
material: item.material ?? '', usage: item.usage ?? item.productUsage ?? '', model: item.model ?? '', brand: item.brand ?? '',
|
|
productLengthCm: item.productLengthCm ?? 0, productWidthCm: item.productWidthCm ?? 0,
|
|
productHeightCm: item.productHeightCm ?? 0, shipmentId: item.shipmentId ?? order.shipmentId ?? '',
|
|
referenceId: item.referenceId ?? order.referenceId ?? '',
|
|
})),
|
|
timeline: detail ? timeline(detail) : [], businessSummary: {
|
|
...businessSummary(),
|
|
totalPackageCount: order.totalPackageCount ?? null,
|
|
actualGrossWeightKg: order.totalGrossWeightKg ?? null,
|
|
actualVolumeCbm: order.totalVolumeCbm ?? null,
|
|
chargeableWeightKg: order.chargeableWeightKg ?? null,
|
|
inboundCode: order.inboundCode ?? '',
|
|
}, createdAt: order.createdAt, updatedAt: order.updatedAt,
|
|
tradeTerm: order.tradeTerm ?? '', destinationCountryCode: order.destinationCountryCode,
|
|
destinationWarehouseId: order.destinationWarehouseId ? String(order.destinationWarehouseId) : '',
|
|
destinationWarehouseName: order.destinationWarehouseName, pickupMethod: order.pickupMethod, channelCode: order.channelCode,
|
|
channelName: order.channelName, carrierId: order.carrierId == null ? '' : String(order.carrierId), carrierName: order.carrierName ?? '',
|
|
pricingTaxMode: order.pricingTaxMode ?? 'TAX_INCLUDED', taxMode: order.taxMode, customsInspection: order.customsInspection,
|
|
destinationType: order.destinationType, amazonWarehouseCode: order.amazonWarehouseCode ?? '', shipmentId: order.shipmentId ?? '',
|
|
requestedPickupAt: '', receivingTime: order.receivingTime ?? '', submittedAt: order.submittedAt ?? '', completedAt: order.completedAt ?? '',
|
|
}
|
|
}
|
|
|
|
function mapUnifiedOrderRow(summary: MerchantUnifiedOrderRow, warehouses: PlatformReceivingWarehouse[]): FirstLegOrder {
|
|
const warehouse = warehouses.find((item) => item.id === String(summary.receivingWarehouseId))
|
|
return {
|
|
id: String(summary.orderId), orderNo: summary.orderNo, referenceNo: summary.referenceId ?? '', transportMode: unifiedTransport(summary.containerMode),
|
|
businessType: 'EXPORT', fulfillmentType: summary.fulfillmentType ?? 'STANDARD', lxCode: summary.lxCode ?? '', origin: '', destination: summary.destinationWarehouseName ?? '',
|
|
receivingWarehouseId: summary.receivingWarehouseId == null ? '' : String(summary.receivingWarehouseId),
|
|
receivingWarehouseName: summary.receivingWarehouseName ?? '', receivingWarehouseSnapshot: warehouseSnapshot(warehouse),
|
|
expectedDepartureDate: summary.expectedShipDate ?? '', etd: '', eta: '', shipperPartyId: '', shipperPartySnapshot: null,
|
|
shipper: '', consigneePartyId: '', consigneePartySnapshot: null, consignee: '', expressCompanyName: summary.expressCompany ?? '',
|
|
trackingNumber: summary.expressTrackingNo ?? summary.delivery?.trackingNo ?? '', skuNos: summary.skuNos ?? [], totalNetWeightKg: summary.totalNetWeightKg ?? null,
|
|
totalQuantity: summary.totalQuantity ?? null, chargeableVolumeCbm: summary.chargeableVolumeCbm ?? null,
|
|
inboundedAt: summary.inboundCompletedAt ?? '', businessDate: '', priority: 'NORMAL', customerRemark: summary.customerRemark ?? '', operatorName: '',
|
|
status: summary.mainStatus, version: 0, items: [], timeline: [], businessSummary: businessSummary(summary),
|
|
createdAt: summary.createdAt, updatedAt: summary.updatedAt, tradeTerm: '', destinationCountryCode: '',
|
|
destinationWarehouseId: summary.destinationWarehouseId == null ? '' : String(summary.destinationWarehouseId),
|
|
destinationWarehouseName: summary.destinationWarehouseName ?? '', pickupMethod: summary.pickupMethod ?? 'SELF_EXPRESS', channelCode: '',
|
|
channelName: summary.channelName ?? '', pricingTaxMode: 'TAX_INCLUDED', carrierId: '', carrierName: '', taxMode: 'BUY_DECLARATION', customsInspection: false,
|
|
destinationType: summary.destinationType ?? 'PRIVATE', amazonWarehouseCode: summary.destinationCode ?? '', shipmentId: summary.shipmentId ?? '',
|
|
requestedPickupAt: summary.requestedPickupAt ?? '', receivingTime: '', submittedAt: '', completedAt: '',
|
|
}
|
|
}
|
|
|
|
function mapFirstLegOrderRow(row: MerchantFirstLegOrderRow): FirstLegOrder {
|
|
const skuNos = Array.isArray(row.skuNos)
|
|
? row.skuNos
|
|
: (row.skuNos ?? '').split('、').map((value) => value.trim()).filter(Boolean)
|
|
const resolvedSkuNos = skuNos.length ? skuNos : (row.cargoes ?? [])
|
|
.map((cargo) => cargo.skuNo?.trim())
|
|
.filter((value): value is string => Boolean(value))
|
|
const uniqueSkuNos = [...new Set(resolvedSkuNos)]
|
|
return {
|
|
id: String(row.orderId), orderNo: row.orderNo, referenceNo: row.referenceId ?? '', transportMode: null,
|
|
businessType: 'EXPORT', origin: '', destination: row.destinationWarehouseName ?? '', receivingWarehouseId: '',
|
|
receivingWarehouseName: '', receivingWarehouseSnapshot: null, expectedDepartureDate: '', etd: '', eta: '',
|
|
shipperPartyId: '', shipperPartySnapshot: null, shipper: '', consigneePartyId: '', consigneePartySnapshot: null,
|
|
consignee: row.receivingCompanyName ?? '', expressCompanyName: row.expressCompany ?? '', trackingNumber: row.trackingNo ?? '',
|
|
skuNos: uniqueSkuNos, totalNetWeightKg: row.totalNetWeightKg, totalQuantity: row.totalQuantity,
|
|
chargeableVolumeCbm: null, inboundedAt: row.inboundedAt ?? '', businessDate: '', priority: 'NORMAL', customerRemark: row.remark ?? '',
|
|
operatorName: '', status: clientStatus(row.status), version: 0, items: [], timeline: [],
|
|
businessSummary: {
|
|
...businessSummary(), totalPackageCount: row.packageCount, actualGrossWeightKg: row.totalGrossWeightKg ?? row.weightKg,
|
|
actualVolumeCbm: row.totalVolumeCbm, chargeableWeightKg: null,
|
|
inboundId: row.inboundId == null ? '' : String(row.inboundId), inboundCode: row.inboundCode ?? '',
|
|
},
|
|
createdAt: row.createdAt, updatedAt: row.createdAt, tradeTerm: '', destinationCountryCode: '', destinationWarehouseId: '',
|
|
destinationWarehouseName: row.destinationWarehouseName ?? '', pickupMethod: row.pickupMethod ?? 'SELF_EXPRESS', channelCode: '',
|
|
channelName: row.channelName ?? '', pricingTaxMode: 'TAX_INCLUDED', carrierId: '', carrierName: '', taxMode: 'BUY_DECLARATION',
|
|
customsInspection: false, destinationType: row.destinationType ?? 'PRIVATE', amazonWarehouseCode: '',
|
|
shipmentId: row.shipmentId ?? row.internationalOrderNo ?? '', requestedPickupAt: '', receivingTime: '', submittedAt: '', completedAt: '',
|
|
}
|
|
}
|
|
|
|
function mapUnifiedOrderDetail(detail: MerchantUnifiedOrderDetail, warehouses: PlatformReceivingWarehouse[]): FirstLegOrder {
|
|
if (!detail?.summary || !detail.order?.order) throw new Error('订单详情数据不完整,请刷新列表后重试')
|
|
const mapped = mapOrder(detail.order.order, warehouses, detail.order)
|
|
mapped.id = String(detail.summary.orderId)
|
|
mapped.status = detail.summary.mainStatus
|
|
mapped.fulfillmentType = detail.summary.fulfillmentType ?? mapped.fulfillmentType
|
|
mapped.lxCode = detail.summary.lxCode ?? mapped.lxCode
|
|
mapped.businessSummary = businessSummary(detail.summary)
|
|
mapped.timeline = unifiedTimeline(detail)
|
|
const summary = mapUnifiedOrderRow(detail.summary, warehouses)
|
|
mapped.referenceNo = summary.referenceNo
|
|
mapped.transportMode = summary.transportMode
|
|
mapped.expressCompanyName = summary.expressCompanyName
|
|
mapped.trackingNumber = summary.trackingNumber
|
|
mapped.skuNos = summary.skuNos
|
|
mapped.totalNetWeightKg = summary.totalNetWeightKg
|
|
mapped.totalQuantity = summary.totalQuantity
|
|
mapped.chargeableVolumeCbm = summary.chargeableVolumeCbm
|
|
mapped.inboundedAt = summary.inboundedAt
|
|
mapped.pickupMethod = summary.pickupMethod
|
|
mapped.requestedPickupAt = summary.requestedPickupAt
|
|
mapped.destinationType = summary.destinationType
|
|
mapped.amazonWarehouseCode = summary.amazonWarehouseCode
|
|
mapped.shipmentId = summary.shipmentId
|
|
return mapped
|
|
}
|
|
|
|
type OrderListReadResult =
|
|
| { source: 'first-leg'; records: MerchantFirstLegOrderRow[] }
|
|
| { source: 'unified'; records: MerchantUnifiedOrderRow[]; drafts: MerchantFirstLegOrderRow[] }
|
|
|
|
interface ClientOrderReadQuery extends MerchantUnifiedOrderQuery {
|
|
fbaCode?: string
|
|
draftOnly?: boolean
|
|
includeDrafts?: boolean
|
|
}
|
|
|
|
function legacyQuery(query: ClientOrderReadQuery, status?: ApiFirstLegStatus) {
|
|
return {
|
|
keyword: query.keyword,
|
|
status,
|
|
createdFrom: query.createdFrom,
|
|
createdTo: query.createdTo,
|
|
}
|
|
}
|
|
|
|
async function getOrdersForClient(query: ClientOrderReadQuery = {}): Promise<OrderListReadResult> {
|
|
if (query.draftOnly) {
|
|
const response = await getMerchantFirstLegOrders(legacyQuery(query, 'DRAFT'))
|
|
return { source: 'first-leg', records: response.records }
|
|
}
|
|
|
|
const unifiedQuery: MerchantUnifiedOrderQuery = {
|
|
keyword: query.keyword,
|
|
orderNo: query.orderNo,
|
|
mainStatus: query.mainStatus,
|
|
receivingWarehouseId: query.receivingWarehouseId,
|
|
destinationWarehouseId: query.destinationWarehouseId,
|
|
inboundCode: query.inboundCode,
|
|
createdFrom: query.createdFrom,
|
|
createdTo: query.createdTo,
|
|
fbaCode: query.fbaCode,
|
|
page: query.page,
|
|
size: query.size,
|
|
}
|
|
try {
|
|
const [response, draftResponse] = await Promise.all([
|
|
getMerchantUnifiedOrders(unifiedQuery),
|
|
query.includeDrafts
|
|
? getMerchantFirstLegOrders(legacyQuery(query, 'DRAFT'))
|
|
: Promise.resolve({ records: [] as MerchantFirstLegOrderRow[] }),
|
|
])
|
|
return { source: 'unified', records: response.records, drafts: draftResponse.records }
|
|
} catch (error) {
|
|
if (!isUnifiedOrderQueryUnavailable(error)) throw error
|
|
const response = await getMerchantFirstLegOrders(legacyQuery(query))
|
|
return { source: 'first-leg', records: response.records }
|
|
}
|
|
}
|
|
|
|
function mapWarehouse(item: MerchantWarehouse): ClientWarehouse {
|
|
return {
|
|
id: String(item.id), code: item.warehouseCode, name: item.warehouseName, province: '',
|
|
city: '', address: '', contactName: item.contactName ?? '', contactPhone: item.contactPhone ?? '',
|
|
businessHours: '', status: item.status, remark: item.remark ?? '', updatedAt: item.updatedAt ?? '',
|
|
warehouseType: item.warehouseType as ClientWarehouseType, countryCode: item.countryCode,
|
|
}
|
|
}
|
|
|
|
export const useFirstLegOrdersStore = defineStore('client-first-leg-orders', () => {
|
|
const orders = ref<FirstLegOrder[]>([])
|
|
const pendingSubmitKeys = new Map<string, string>()
|
|
const warehouses = ref<ClientWarehouse[]>([])
|
|
const loading = ref(false)
|
|
const loadedFromApi = ref(false)
|
|
const loadError = ref('')
|
|
const warehouseLoadError = ref('')
|
|
const draftLoadError = ref('')
|
|
const loadedWarehouseTypes = ref<ClientWarehouseType[]>([])
|
|
const warehouseTypeRequests = new Map<ClientWarehouseType, Promise<ClientWarehouse[]>>()
|
|
const activeCount = computed(() => orders.value.filter((item) => item.status !== 'DRAFT' && item.status !== 'DELIVERED').length)
|
|
const pendingCount = computed(() => orders.value.filter((item) => item.status === 'PENDING_REVIEW').length)
|
|
|
|
function getById(id: string) { return orders.value.find((item) => item.id === id) }
|
|
|
|
async function loadWarehouseType(warehouseType: ClientWarehouseType, force = false) {
|
|
if (warehouseType === 'DOMESTIC' && force) warehouseLoadError.value = ''
|
|
if (!force && loadedWarehouseTypes.value.includes(warehouseType)) return
|
|
const pending = warehouseTypeRequests.get(warehouseType)
|
|
if (pending) return pending
|
|
|
|
const request = (async () => {
|
|
const records = await loadAllWarehouseRecords(warehouseType, getMerchantWarehouses)
|
|
return records
|
|
.filter((item) => item.status === 'ACTIVE' && item.warehouseType === warehouseType)
|
|
.map(mapWarehouse)
|
|
})()
|
|
warehouseTypeRequests.set(warehouseType, request)
|
|
try {
|
|
const nextWarehouses = await request
|
|
warehouses.value = [...warehouses.value.filter((item) => item.warehouseType !== warehouseType), ...nextWarehouses]
|
|
loadedWarehouseTypes.value = [...loadedWarehouseTypes.value.filter((item) => item !== warehouseType), warehouseType]
|
|
} catch (error) {
|
|
if (warehouseType === 'DOMESTIC') warehouseLoadError.value = error instanceof Error ? error.message : '公共仓库数据加载失败'
|
|
throw error
|
|
} finally {
|
|
if (warehouseTypeRequests.get(warehouseType) === request) warehouseTypeRequests.delete(warehouseType)
|
|
}
|
|
}
|
|
|
|
async function load(query: ClientOrderReadQuery = {}) {
|
|
loading.value = true
|
|
loadedFromApi.value = true
|
|
loadError.value = ''
|
|
warehouseLoadError.value = ''
|
|
draftLoadError.value = ''
|
|
loadedWarehouseTypes.value = []
|
|
warehouseTypeRequests.clear()
|
|
orders.value = []
|
|
warehouses.value = []
|
|
const [orderResult, warehouseResult] = await Promise.allSettled([getOrdersForClient(query), loadWarehouseType('DOMESTIC', true)])
|
|
if (warehouseResult.status === 'rejected' && !warehouseLoadError.value) warehouseLoadError.value = warehouseResult.reason instanceof Error ? warehouseResult.reason.message : '公共仓库数据加载失败'
|
|
if (orderResult.status === 'fulfilled') {
|
|
orders.value = orderResult.value.source === 'unified'
|
|
? [
|
|
...orderResult.value.drafts.map(mapFirstLegOrderRow),
|
|
...orderResult.value.records.map((item) => mapUnifiedOrderRow(item, warehouses.value)),
|
|
]
|
|
: orderResult.value.records.map(mapFirstLegOrderRow)
|
|
} else {
|
|
loadError.value = orderResult.reason instanceof Error ? orderResult.reason.message : '订单数据加载失败'
|
|
}
|
|
loading.value = false
|
|
return orders.value.length
|
|
}
|
|
|
|
function upsertOrder(mapped: FirstLegOrder) {
|
|
const index = orders.value.findIndex((item) => item.id === mapped.id)
|
|
if (index >= 0) orders.value[index] = mapped
|
|
else orders.value.unshift(mapped)
|
|
return mapped
|
|
}
|
|
|
|
async function loadLegacyDetail(id: string) {
|
|
const detail = await getMerchantFirstLegOrder(id)
|
|
return upsertOrder(mapOrder(detail.order, warehouses.value, detail))
|
|
}
|
|
|
|
async function loadUnifiedDetail(id: string) {
|
|
try {
|
|
const detail = await getMerchantUnifiedOrder(id)
|
|
return upsertOrder(mapUnifiedOrderDetail(detail, warehouses.value))
|
|
} catch (error) {
|
|
if (!isUnifiedOrderQueryUnavailable(error)) throw error
|
|
return loadLegacyDetail(id)
|
|
}
|
|
}
|
|
|
|
async function loadDetail(id: string) {
|
|
return getById(id)?.status === 'DRAFT' ? loadLegacyDetail(id) : loadUnifiedDetail(id)
|
|
}
|
|
|
|
async function loadWarehouseDetail(id: string) {
|
|
const detail = await getMerchantWarehouseDetail(id)
|
|
const mapped = mapWarehouse(detail.warehouse)
|
|
const address = detail.addresses[0]
|
|
const receivingCapability = detail.capabilities.find((item) => item.enabled && item.cutoffTime)
|
|
if (address) {
|
|
mapped.province = address.stateProvince ?? ''
|
|
mapped.city = address.city ?? ''
|
|
mapped.address = [address.district, address.addressLine1, address.addressLine2].filter(Boolean).join(' ')
|
|
}
|
|
if (receivingCapability?.cutoffTime) mapped.businessHours = `每日 ${receivingCapability.cutoffTime.slice(0, 5)} 前`
|
|
const index = warehouses.value.findIndex((item) => item.id === id)
|
|
if (index >= 0) warehouses.value[index] = mapped
|
|
return mapped
|
|
}
|
|
|
|
async function saveDraft(draft: FirstLegOrderDraft, customerName: string, id?: string) {
|
|
const warehouse = prepareDraftForSave(draft, warehouses.value)
|
|
if (id) {
|
|
const order = getById(id)
|
|
if (!order || !canClientEditFirstLegOrder(order.status)) throw new Error('当前订单状态不允许编辑')
|
|
await updateMerchantFirstLegOrder(id, savePayload(draft, warehouse, customerName))
|
|
return loadLegacyDetail(id)
|
|
}
|
|
const createdId = await createMerchantFirstLegOrder(savePayload(draft, warehouse, customerName))
|
|
return loadLegacyDetail(String(createdId))
|
|
}
|
|
|
|
async function saveDraftBatch(
|
|
drafts: Array<{ clientRowId: string; draft: FirstLegOrderDraft }>,
|
|
customerName: string,
|
|
idempotencyKey: string,
|
|
): Promise<MerchantBatchDraftResponse> {
|
|
const items = drafts.map(({ clientRowId, draft }) => {
|
|
const warehouse = prepareDraftForSave(draft, warehouses.value)
|
|
return { clientRowId, draft: savePayload(draft, warehouse, customerName) }
|
|
})
|
|
return createMerchantFirstLegOrderBatch(items, idempotencyKey)
|
|
}
|
|
|
|
async function submitSaved(id: string, companyAuditStatus?: MerchantTenantStatus) {
|
|
if (companyAuditStatus && companyAuditStatus !== 'ACTIVE') throw new Error('企业资料尚未审核通过,请先完善企业资料并等待平台审核')
|
|
const idempotencyKey = pendingSubmitKeys.get(id) ?? createIdempotencyKey(`first-leg-submit-${id}`)
|
|
pendingSubmitKeys.set(id, idempotencyKey)
|
|
await submitMerchantFirstLegOrder(id, idempotencyKey)
|
|
pendingSubmitKeys.delete(id)
|
|
return loadUnifiedDetail(id)
|
|
}
|
|
|
|
async function submit(draft: FirstLegOrderDraft, customerName: string, id?: string, companyAuditStatus?: MerchantTenantStatus) {
|
|
const order = await saveDraft(draft, customerName, id)
|
|
return submitSaved(order.id, companyAuditStatus)
|
|
}
|
|
|
|
async function loadDraftDetail(id: string) {
|
|
return loadLegacyDetail(id)
|
|
}
|
|
|
|
return { orders, warehouses, loading, loadedFromApi, loadError, warehouseLoadError, draftLoadError, activeCount, pendingCount, getById, load, loadDetail, loadDraftDetail, loadWarehouseDetail, loadWarehouseType, saveDraft, saveDraftBatch, submitSaved, submit }
|
|
})
|