Files
client-web/src/modules/first-leg/store/firstLegOrders.ts
T
2026-08-03 18:52:30 +08:00

258 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
import {
createMerchantFirstLegOrder,
getMerchantFirstLegOrder,
getMerchantFirstLegOrders,
getMerchantWarehouseDetail,
getMerchantWarehouses,
submitMerchantFirstLegOrder,
updateMerchantFirstLegOrder,
type ApiFirstLegStatus,
type FirstLegOrderPartyPayload,
type FirstLegOrderSaveRequest,
type MerchantFirstLegOrder,
type MerchantFirstLegOrderDetail,
type MerchantWarehouse,
} from '@/modules/first-leg/api/firstLeg'
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 } from '@/modules/first-leg/utils/firstLegOrder'
const countryCodes: Readonly<Record<string, string>> = {
中国: 'CN', 美国: 'US', 德国: 'DE', 英国: 'GB', 加拿大: 'CA', 日本: 'JP', 澳大利亚: 'AU',
}
function clientStatus(status: ApiFirstLegStatus): FirstLegOrder['status'] {
if (status === 'SUBMITTED') return 'PENDING_REVIEW'
// 后端联调任务曾出现 REJECTED,但一期已确认订单不设驳回状态;兼容旧数据时仍按待审核展示。
if (status === 'REJECTED') return 'PENDING_REVIEW'
if (status === 'APPROVED' || status === 'ACCEPTED') return 'ACCEPTED'
if (status === 'PROCESSING') return 'IN_TRANSIT'
return status
}
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 apiTransport(draft: FirstLegOrderDraft): FirstLegOrderSaveRequest['transportMode'] {
if (draft.transportMode === 'AIR') return 'AIR'
if (draft.transportMode === 'ROAD') return 'TRUCK'
return 'SEA'
}
function partyPayload(snapshot: FirstLegPartySnapshot): FirstLegOrderPartyPayload {
return {
companyName: snapshot.name,
contactName: snapshot.contactName,
phone: snapshot.phone,
email: snapshot.email || undefined,
countryCode: countryCodes[snapshot.countryRegion] ?? snapshot.countryRegion.toUpperCase().slice(0, 2),
province: snapshot.province || undefined,
city: snapshot.city,
addressLine: snapshot.address,
postalCode: snapshot.postalCode || undefined,
}
}
function savePayload(draft: FirstLegOrderDraft, warehouse: PlatformReceivingWarehouse, customerName: string): FirstLegOrderSaveRequest {
if (!draft.shipperPartySnapshot || !draft.consigneePartySnapshot) throw new Error('请完整填写收发货信息')
return {
// 后端把 customerId 作为订单主往来单位校验并以服务端名称覆盖 customerName;
// 当前商户下单以所选发货单位作为主往来单位,收发货详情仍分别保存订单快照。
customerId: draft.shipperPartyId,
customerName,
transportMode: apiTransport(draft),
tradeTerm: draft.tradeTerm || undefined,
destinationCountryCode: draft.destinationCountryCode.trim().toUpperCase(),
destinationWarehouseName: draft.destinationWarehouseName,
expectedShipDate: draft.expectedDepartureDate || undefined,
customerRemark: draft.customerRemark || undefined,
sender: partyPayload(draft.shipperPartySnapshot),
receiver: partyPayload(draft.consigneePartySnapshot),
cargoes: draft.items.map((item) => ({
cargoName: item.cargoNameCn,
cargoNameEn: item.cargoNameEn || undefined,
hsCode: item.hsCode || undefined,
packageType: item.packageType,
packageCount: item.packageCount,
grossWeightKg: item.grossWeightKg,
volumeCbm: item.volumeCbm,
declaredValue: item.declaredValue || undefined,
currency: item.currency || undefined,
dangerousGoods: item.dangerousGoods,
cargoRemark: item.remark || undefined,
skuNo: item.sku || undefined,
upcNo: item.upc || undefined,
totalQuantity: item.pieces,
})),
pickupMethod: draft.pickupMethod,
receivingWarehouseId: warehouse.id,
receivingWarehouseName: warehouse.name,
channelCode: draft.channelCode,
channelName: draft.channelName,
taxMode: draft.taxMode,
containerMode: draft.transportMode === 'FCL' ? 'FULL_CONTAINER' : draft.transportMode === 'LCL' ? 'LCL' : undefined,
customsInspection: draft.customsInspection,
destinationType: draft.destinationType,
amazonWarehouseCode: draft.amazonWarehouseCode || undefined,
shipmentId: draft.shipmentId || undefined,
referenceId: draft.referenceNo || undefined,
}
}
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) => ({
id: String(item.id),
event: item.toStatus === 'SUBMITTED' || item.toStatus === 'REJECTED' ? 'SUBMITTED' : item.toStatus === 'CANCELLED' ? 'CANCELLED' : item.toStatus === 'COMPLETED' ? 'COMPLETED' : item.toStatus === 'ACCEPTED' || item.toStatus === 'APPROVED' ? 'ACCEPTED' : 'IN_TRANSIT',
title: item.toStatus === 'REJECTED' ? '平台要求补充订单信息' : item.action || `状态更新为 ${item.toStatus}`,
description: item.reason ?? '',
operatorName: item.operatorName ?? '',
occurredAt: item.occurredAt,
}))
}
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), orderNo: order.orderNo, referenceNo: order.referenceId ?? '', transportMode: clientTransport(order),
businessType: 'EXPORT', 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 ?? '', 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 ?? '',
pieces: item.totalQuantity ?? item.packageCount, packageCount: item.packageCount, grossWeightKg: item.grossWeightKg,
volumeCbm: item.volumeCbm, containerType: '', containerCount: 0, remark: item.cargoRemark ?? '',
packageType: item.packageType, hsCode: item.hsCode ?? '', declaredValue: item.declaredValue ?? 0,
currency: item.currency ?? 'USD', dangerousGoods: item.dangerousGoods ?? false,
})),
timeline: detail ? timeline(detail) : [], createdAt: order.createdAt, updatedAt: order.updatedAt,
tradeTerm: order.tradeTerm ?? '', destinationCountryCode: order.destinationCountryCode,
destinationWarehouseName: order.destinationWarehouseName, pickupMethod: order.pickupMethod, channelCode: order.channelCode,
channelName: order.channelName, taxMode: order.taxMode, customsInspection: order.customsInspection,
destinationType: order.destinationType, amazonWarehouseCode: order.amazonWarehouseCode ?? '', shipmentId: order.shipmentId ?? '',
}
}
function mapWarehouse(item: MerchantWarehouse): PlatformReceivingWarehouse {
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 ?? '',
}
}
export const useFirstLegOrdersStore = defineStore('client-first-leg-orders', () => {
const orders = ref<FirstLegOrder[]>([])
const warehouses = ref<PlatformReceivingWarehouse[]>([])
const loading = ref(false)
const loadedFromApi = ref(false)
const loadError = ref('')
const activeCount = computed(() => orders.value.filter((item) => ['PENDING_REVIEW', 'ACCEPTED', 'BOOKED', 'IN_TRANSIT'].includes(item.status)).length)
const pendingCount = computed(() => orders.value.filter((item) => item.status === 'PENDING_REVIEW').length)
function getById(id: string) { return orders.value.find((item) => item.id === id) }
async function load() {
loading.value = true
loadedFromApi.value = true
loadError.value = ''
orders.value = []
warehouses.value = []
try {
const [orderPage, warehousePage] = await Promise.all([getMerchantFirstLegOrders(), getMerchantWarehouses()])
warehouses.value = warehousePage.records.filter((item) => item.status === 'ACTIVE').map(mapWarehouse)
orders.value = orderPage.records.map((item) => mapOrder(item, warehouses.value))
} catch (error) {
loadError.value = error instanceof Error ? error.message : '订单与仓库数据加载失败'
throw error
} finally { loading.value = false }
}
async function loadDetail(id: string) {
const detail = await getMerchantFirstLegOrder(id)
const mapped = mapOrder(detail.order, warehouses.value, detail)
const index = orders.value.findIndex((item) => item.id === id)
if (index >= 0) orders.value[index] = mapped
else orders.value.unshift(mapped)
return mapped
}
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 = warehouses.value.find((item) => item.id === draft.receivingWarehouseId)
if (!warehouse) throw new Error('所选收货仓当前不可用')
if (id) {
const order = getById(id)
if (!order || !canClientEditFirstLegOrder(order.status)) throw new Error('当前订单状态不允许编辑')
await updateMerchantFirstLegOrder(id, savePayload(draft, warehouse, customerName))
return loadDetail(id)
}
const createdId = await createMerchantFirstLegOrder(savePayload(draft, warehouse, customerName))
return loadDetail(String(createdId))
}
async function submit(draft: FirstLegOrderDraft, customerName: string, id?: string, companyAuditStatus: MerchantTenantStatus = 'ACTIVE') {
if (companyAuditStatus !== 'ACTIVE') throw new Error('企业资料尚未审核通过,请先完善企业资料并等待平台审核')
const order = await saveDraft(draft, customerName, id)
await submitMerchantFirstLegOrder(order.id)
return loadDetail(order.id)
}
return { orders, warehouses, loading, loadedFromApi, loadError, activeCount, pendingCount, getById, load, loadDetail, loadWarehouseDetail, saveDraft, submit }
})