From 907794f9a7f638dd18fd4be31b5af5d13792abe6 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Thu, 30 Jul 2026 18:08:40 +0800 Subject: [PATCH] feat: improve client order and member workflows --- src/api/firstLeg.ts | 22 +--------- src/stores/firstLegOrders.ts | 37 ++++++++++++++-- src/views/FirstLegOrdersView.vue | 72 +++++++++++++++++++++++++++++++- src/views/MembersView.vue | 13 +++--- 4 files changed, 112 insertions(+), 32 deletions(-) diff --git a/src/api/firstLeg.ts b/src/api/firstLeg.ts index 4f08cc0..c56d3cc 100644 --- a/src/api/firstLeg.ts +++ b/src/api/firstLeg.ts @@ -54,23 +54,6 @@ export interface MerchantWarehouseDetail { capabilities: MerchantWarehouseCapability[] } -export interface MerchantPartner { - id: number | string - partnerCode: string - partnerName: string - partnerType: string - contactName: string | null - contactPhone: string | null - contactEmail: string | null - countryCode: string - province: string | null - city: string | null - district: string | null - addressLine: string - postalCode: string | null - status: 'ACTIVE' | 'INACTIVE' -} - export interface FirstLegOrderPartyPayload { companyName: string contactName: string @@ -146,6 +129,7 @@ export interface MerchantFirstLegOrder { pickupMethod: ApiPickupMethod receivingWarehouseId: number | string receivingWarehouseName: string + receivingWarehouseAddress?: string | null channelCode: string channelName: string taxMode: ApiTaxMode @@ -209,10 +193,6 @@ export function getMerchantWarehouseDetail(id: number | string) { return apiRequest({ method: 'GET', url: `/merchant/v1/warehouses/${id}` }) } -export function getMerchantPartnerCandidates(type: 'SHIPPER' | 'CONSIGNEE') { - return apiRequest({ method: 'GET', url: '/merchant/v1/partners/candidates', params: { type } }) -} - export function getMerchantFirstLegOrders(params: FirstLegOrderQuery = {}) { return apiRequest>({ method: 'GET', url: '/merchant/v1/orders/first-leg', params: { page: 1, size: 200, ...params } }) } diff --git a/src/stores/firstLegOrders.ts b/src/stores/firstLegOrders.ts index 9eb6755..64f9d24 100644 --- a/src/stores/firstLegOrders.ts +++ b/src/stores/firstLegOrders.ts @@ -4,6 +4,7 @@ import { createMerchantFirstLegOrder, getMerchantFirstLegOrder, getMerchantFirstLegOrders, + getMerchantWarehouseDetail, getMerchantWarehouses, submitMerchantFirstLegOrder, updateMerchantFirstLegOrder, @@ -105,6 +106,20 @@ function warehouseSnapshot(warehouse?: PlatformReceivingWarehouse): PlatformRece 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 @@ -134,10 +149,10 @@ function mapOrder(order: MerchantFirstLegOrder, warehouses: PlatformReceivingWar 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: warehouseSnapshot(warehouse), expectedDepartureDate: order.expectedShipDate ?? '', etd: '', eta: '', + 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: '', trackingNumber: '', priority: 'NORMAL', customerRemark: order.customerRemark ?? '', operatorName: '待平台分配', + 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 ?? '', cargoNameCn: item.cargoName, cargoNameEn: item.cargoNameEn ?? '', @@ -198,6 +213,22 @@ export const useFirstLegOrdersStore = defineStore('client-first-leg-orders', () 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('所选收货仓当前不可用') @@ -218,5 +249,5 @@ export const useFirstLegOrdersStore = defineStore('client-first-leg-orders', () return loadDetail(order.id) } - return { orders, warehouses, loading, loadedFromApi, loadError, activeCount, pendingCount, getById, load, loadDetail, saveDraft, submit } + return { orders, warehouses, loading, loadedFromApi, loadError, activeCount, pendingCount, getById, load, loadDetail, loadWarehouseDetail, saveDraft, submit } }) diff --git a/src/views/FirstLegOrdersView.vue b/src/views/FirstLegOrdersView.vue index d7ca89f..164b357 100644 --- a/src/views/FirstLegOrdersView.vue +++ b/src/views/FirstLegOrdersView.vue @@ -75,6 +75,8 @@ function emptyDraft(): FirstLegOrderDraft { } const form = reactive(emptyDraft()) +const serializedDraft = computed(() => JSON.stringify(form)) +const editorDirty = computed(() => editorOpen.value && serializedDraft.value !== editorBaseline.value) const filteredOrders = computed(() => store.orders.filter((order) => { const keyword = filters.keyword.trim().toLowerCase() @@ -90,8 +92,10 @@ const filteredOrders = computed(() => store.orders.filter((order) => { const detailOrder = computed(() => detailId.value ? store.getById(detailId.value) : undefined) const detailSummary = computed(() => detailOrder.value ? summarizeFirstLegCargo(detailOrder.value.items) : null) const selectedWarehouse = computed(() => availableWarehouses.value.find((item) => item.id === form.receivingWarehouseId)) +const hasActiveFilters = computed(() => Boolean(filters.keyword.trim() || filters.status || filters.transportMode || dateRange.value.length)) +const emptyDescription = computed(() => hasActiveFilters.value ? '没有符合筛选条件的头程订单' : '还没有头程订单') -onMounted(async () => { +async function loadPageData() { try { await Promise.all([store.load(), firstLegPartySource.load()]) availableShippers.value = firstLegPartySource.listAvailable('SHIPPER') @@ -99,10 +103,33 @@ onMounted(async () => { } catch (error) { ElMessage.error(error instanceof Error ? error.message : '订单与往来单位数据加载失败') } +} + +function handleBeforeUnload(event: BeforeUnloadEvent) { + if (!editorDirty.value) return + event.preventDefault() + event.returnValue = '' +} + +onMounted(() => { + window.addEventListener('beforeunload', handleBeforeUnload) + void loadPageData() }) +onBeforeUnmount(() => window.removeEventListener('beforeunload', handleBeforeUnload)) + +onBeforeRouteLeave(async () => { + if (!editorDirty.value) return true + return confirmDiscardChanges() +}) + +function markEditorClean() { + editorBaseline.value = serializedDraft.value +} + function replaceForm(draft: FirstLegOrderDraft) { Object.assign(form, structuredClone(draft)) + markEditorClean() } function draftFromOrder(order: FirstLegOrder): FirstLegOrderDraft { @@ -180,6 +207,15 @@ function clearConsignee() { clearParty('CONSIGNEE') } +async function loadSelectedWarehouse(id: string) { + if (!id) return + try { + await store.loadWarehouseDetail(id) + } catch (error) { + ElMessage.error(error instanceof Error ? error.message : '仓库地址与收货时间加载失败') + } +} + function openCreate() { if (availableWarehouses.value.length === 0) { ElMessage.warning('当前没有可用的平台收货仓,请联系平台维护仓库后再创建订单') @@ -202,15 +238,47 @@ async function openDetail(order: FirstLegOrder) { detailId.value = order.id detailOpen.value = true detailLoading.value = true + detailError.value = '' try { await store.loadDetail(order.id) } catch (error) { - ElMessage.error(error instanceof Error ? error.message : '订单详情加载失败') + detailError.value = error instanceof Error ? error.message : '订单详情加载失败' + ElMessage.error(detailError.value) } finally { detailLoading.value = false } } +async function retryDetail() { + const order = detailId.value ? store.getById(detailId.value) : undefined + if (order) await openDetail(order) +} + +async function confirmDiscardChanges() { + try { + await ElMessageBox.confirm('当前订单还有未保存的修改。离开后这些内容将丢失。', '放弃未保存的修改?', { + type: 'warning', + confirmButtonText: '放弃修改', + cancelButtonText: '继续编辑', + }) + return true + } catch (error) { + if (error === 'cancel' || error === 'close') return false + throw error + } +} + +async function requestCloseEditor(done: () => void) { + if (saving.value) return + if (!editorDirty.value || await confirmDiscardChanges()) done() +} + +function closeEditor() { + void requestCloseEditor(() => { + editorOpen.value = false + }) +} + function addCargoItem() { form.items.push(createEmptyCargoItem(`new-cargo-${++cargoSequence}`)) } diff --git a/src/views/MembersView.vue b/src/views/MembersView.vue index 8b3dd6a..177d5a1 100644 --- a/src/views/MembersView.vue +++ b/src/views/MembersView.vue @@ -134,12 +134,12 @@ function clearFilters() {
- - - - - - + + + + + +
@@ -181,6 +181,7 @@ function clearFilters() { .member-identity { display: flex; align-items: center; gap: 10px; } .member-identity > span { display: grid; width: 34px; height: 34px; flex: 0 0 34px; place-items: center; border-radius: 50%; background: #e7f3f1; color: var(--primary); font-weight: 750; } .member-identity > div, .member-contact { display: flex; min-width: 0; flex-direction: column; gap: 3px; } +.member-identity strong, .member-identity small, .member-contact small { overflow-wrap: anywhere; } .member-identity small, .member-contact small { color: var(--muted); font-size: 10px; } .role-tags { display: flex; gap: 5px; flex-wrap: wrap; } .mobile-member-list { display: none; }