diff --git a/src/modules/overseas-business/CONTRACT_GAPS.md b/src/modules/overseas-business/CONTRACT_GAPS.md index d9246080..c6dc29bd 100644 --- a/src/modules/overseas-business/CONTRACT_GAPS.md +++ b/src/modules/overseas-business/CONTRACT_GAPS.md @@ -12,7 +12,7 @@ 明确不接入: - 采购审核 `review`:后端存在,但当前页面原型与本专项范围排除。 -- 采购出库:后端已移除 `/outbounds`,四期不承载采购出库或库存离仓。 +- 采购出库不提供独立 `/outbounds` 端点;采购编辑通过出库明细维护出库量并派生待出库、部分出库、全部出库状态,不直接操作海外仓 PDA 可用库存。 - 卡车状态推进、收托盘和散板状态推进:后端存在,但当前原型没有对应操作承载方式。 - 海外出库、海外派送和 Merchant API。 diff --git a/src/modules/overseas-business/api/platformOverseasBusiness.test.ts b/src/modules/overseas-business/api/platformOverseasBusiness.test.ts index 26ae5f4d..a99aef26 100644 --- a/src/modules/overseas-business/api/platformOverseasBusiness.test.ts +++ b/src/modules/overseas-business/api/platformOverseasBusiness.test.ts @@ -15,7 +15,7 @@ const envelope = (data: T) => ({ code: '200', message: 'success', data }) const purchaseSummary = { id: '9007199254740993', orderNo: 'OP-1', customerId: '10001', customerName: '客户一', purchaseCategory: '服装', productName: '外套', productSpecification: null, purchaseQuantity: 10, budgetUnitPrice: 12.34, currency: 'USD', purchaseWarehouseId: '9007199254740995', - purchaseWarehouseName: '采购仓', inboundQuantity: 5, inventoryQuantity: 5, status: 'APPROVED', version: 4, + purchaseWarehouseName: '采购仓', inboundQuantity: 5, outboundQuantity: 0, inventoryQuantity: 5, status: 'PENDING_OUTBOUND', version: 4, createdAt: '2026-08-01T00:00:00', updatedAt: '2026-08-02T00:00:00', } const truckSummary = { diff --git a/src/modules/overseas-business/contract.test.ts b/src/modules/overseas-business/contract.test.ts index be382783..e6117a30 100644 --- a/src/modules/overseas-business/contract.test.ts +++ b/src/modules/overseas-business/contract.test.ts @@ -22,6 +22,7 @@ describe('overseas business real contract pages', () => { expect(detail).not.toContain('outbounds') expect(edit).toContain('estimatedArrivalAt: form.estimatedArrivalAt || null') expect(edit).toContain('purchaseWarehouseId: form.purchaseWarehouseId || null') + expect(edit).not.toContain('入库量不能小于 0') expect(edit).not.toMatch(/payload[^\n]*(outboundQuantity|status):/) }) diff --git a/src/modules/overseas-business/pages/OverseasBusinessViews.test.ts b/src/modules/overseas-business/pages/OverseasBusinessViews.test.ts index 5208328f..db746f37 100644 --- a/src/modules/overseas-business/pages/OverseasBusinessViews.test.ts +++ b/src/modules/overseas-business/pages/OverseasBusinessViews.test.ts @@ -33,7 +33,7 @@ function memoryStorage(): Storage { const values = new Map(); re const purchase: OverseasPurchaseDetail = { id: '9007199254740993', orderNo: 'OP-100', customerId: '10001', customerName: '客户甲', purchaseCategory: '家居', productName: '置物架', productSpecification: '黑色', productUrl: 'https://example.test/product', - purchaseQuantity: 20, budgetUnitPrice: '12.50', currency: 'USD', purchaseWarehouseId: '20001', purchaseWarehouseName: '洛杉矶采购仓', inboundQuantity: 10, inventoryQuantity: 10, status: 'APPROVED', version: 3, + purchaseQuantity: 20, budgetUnitPrice: '12.50', currency: 'USD', purchaseWarehouseId: '20001', purchaseWarehouseName: '洛杉矶采购仓', inboundQuantity: 10, outboundQuantity: 0, inventoryQuantity: 10, status: 'PENDING_OUTBOUND', version: 3, createdAt: '2026-08-01T10:00:00', updatedAt: '2026-08-02T10:00:00', receivingAddress: 'LA', contactName: 'Alice', contactPhone: '10086', domesticCarrier: null, domesticTrackingNo: null, overseasCarrier: 'UPS', overseasTrackingNo: 'UPS-1', estimatedArrivalAt: '2026-08-25T12:00:00', remark: '采购备注', createdBy: '7', reviewedBy: '8', reviewRemark: null, reviewedAt: '2026-08-02T09:00:00', histories: [{ operationType: 'UPDATE', fromStatus: 'APPROVED', toStatus: 'APPROVED', operatorId: '7', changeDetail: '{"remark":"采购备注"}', remark: null, createdAt: '2026-08-02T10:00:00' }], @@ -58,7 +58,7 @@ describe('overseas business mounted pages', () => { vi.mocked(listPlatformTenants).mockReset().mockResolvedValue({ records: [{ id: '10001', tenantName: '可选客户', status: 'ACTIVE' } as never], page: 1, size: 200, total: 1, pages: 1 }) }) - it('mounts procurement list with customer candidates and real data', async () => { const wrapper = mountPage(PurchaseOrdersView); await flushPromises(); expect(listOverseasPurchaseCustomerOptions).toHaveBeenCalled(); expect(listOverseasPurchases).toHaveBeenCalledWith({ page: 1, size: 20, customerId: '', orderNo: '', purchaseCategory: '', productName: '' }); expect(wrapper.text()).toContain('OP-100'); expect(wrapper.text()).toContain('已通过'); expect(wrapper.text()).toContain('2026-08-01 10:00') }) + it('mounts procurement list with customer candidates and real data', async () => { const wrapper = mountPage(PurchaseOrdersView); await flushPromises(); expect(listOverseasPurchaseCustomerOptions).toHaveBeenCalled(); expect(listOverseasPurchases).toHaveBeenCalledWith({ page: 1, size: 20, customerId: '', orderNo: '', purchaseCategory: '', productName: '' }); expect(wrapper.text()).toContain('OP-100'); expect(wrapper.text()).toContain('待出库'); expect(wrapper.text()).toContain('2026-08-01 10:00') }) it('paginates procurement results', async () => { vi.mocked(listOverseasPurchases).mockResolvedValue({ records: [purchase], page: 1, size: 20, total: 40, pages: 2 }); const wrapper = mountPage(PurchaseOrdersView); await flushPromises(); await wrapper.find('button[aria-label="第 2 页"]').trigger('click'); await flushPromises(); expect(listOverseasPurchases).toHaveBeenLastCalledWith(expect.objectContaining({ page: 2 })) }) it('renders procurement empty and traceable 403 states', async () => { vi.mocked(listOverseasPurchases).mockResolvedValueOnce({ records: [], page: 1, size: 20, total: 0, pages: 0 }); let wrapper = mountPage(PurchaseOrdersView); await flushPromises(); expect(wrapper.text()).toContain('暂无符合筛选条件的海外采购订单'); wrapper.unmount(); vi.mocked(listOverseasPurchases).mockRejectedValue(new ApiResponseError('forbidden', 'A0403', 403, null, 'REQ-403')); wrapper = mountPage(PurchaseOrdersView); await flushPromises(); expect(wrapper.text()).toContain('当前账号无权访问该数据'); expect(wrapper.text()).toContain('REQ-403') }) it('mounts procurement detail operation history', async () => { routing.route.params = { purchaseId: 'OP-100' }; routing.route.query = { customerId: '10001' }; const wrapper = mountPage(PurchaseOrderDetailView); await flushPromises(); expect(getOverseasPurchase).toHaveBeenCalledWith('OP-100', '10001'); expect(wrapper.text()).toContain('操作流水'); expect(wrapper.text()).toContain('编辑') }) diff --git a/src/modules/overseas-business/pages/PurchaseOrderDetailView.vue b/src/modules/overseas-business/pages/PurchaseOrderDetailView.vue index 75d2e433..11ca8764 100644 --- a/src/modules/overseas-business/pages/PurchaseOrderDetailView.vue +++ b/src/modules/overseas-business/pages/PurchaseOrderDetailView.vue @@ -15,11 +15,12 @@ const route = useRoute(); const router = useRouter() const purchaseId = computed(() => typeof route.params.purchaseId === 'string' ? route.params.purchaseId : '') const customerId = computed(() => typeof route.query.customerId === 'string' ? route.query.customerId : '') const detail = ref(null); const loading = ref(false); const loadError = ref(null) -const statusLabels: Record = { PENDING_REVIEW: '待审核', APPROVED: '已通过', REJECTED: '已拒绝' } +const statusLabels: Record = { PENDING_REVIEW: '待审核', APPROVED: '已通过', REJECTED: '已拒绝', PENDING_OUTBOUND: '待出库', PARTIALLY_OUTBOUND: '部分出库', OUTBOUNDED: '全部出库' } const productFields = computed(() => [['客户名称', detail.value?.customerName], ['采购仓库', detail.value?.purchaseWarehouseName], ['采购品类', detail.value?.purchaseCategory], ['商品名称', detail.value?.productName], ['商品规格', detail.value?.productSpecification], ['商品链接', detail.value?.productUrl]] as const) -const stockFields = computed(() => [['入库量', detail.value?.inboundQuantity], ['库存量', detail.value?.inventoryQuantity], ['库存状态', detail.value ? (detail.value.inventoryQuantity > 0 ? '在库' : '无库存') : null]] as const) +const stockFields = computed(() => [['入库量', detail.value?.inboundQuantity], ['出库量', detail.value?.outboundQuantity], ['库存量', detail.value?.inventoryQuantity], ['库存状态', detail.value ? (detail.value.inventoryQuantity > 0 ? '在库' : '无库存') : null]] as const) const orderFields = computed(() => [['订单编号', detail.value?.orderNo], ['采购数量', detail.value?.purchaseQuantity], ['预算单价', detail.value ? formatAmount(detail.value.budgetUnitPrice, detail.value.currency) : null], ['订单状态', detail.value ? statusLabels[detail.value.status] : null], ['联系人', detail.value?.contactName], ['联系电话', detail.value?.contactPhone], ['收货地址', detail.value?.receivingAddress], ['订单备注', detail.value?.remark]] as const) const courierFields = computed(() => [['国内承运商', detail.value?.domesticCarrier], ['国内运单号', detail.value?.domesticTrackingNo], ['海外承运商', detail.value?.overseasCarrier], ['海外运单号', detail.value?.overseasTrackingNo], ['预计到达时间', formatDateTime(detail.value?.estimatedArrivalAt)]] as const) +const outboundDetails = computed(() => detail.value?.outboundDetails || []) const historyColumns: DataTableColumn[] = [ { key: 'createdAt', label: '时间', minWidth: 155, formatter: (row: OverseasBusinessHistory) => formatDateTime(row.createdAt) }, { key: 'operationType', label: '操作类型', width: 110, formatter: (row: OverseasBusinessHistory) => operationLabels[row.operationType || ''] || row.operationType || '—' }, @@ -52,7 +53,8 @@ watch([purchaseId, customerId], () => { void load() }, { immediate: true })

库存信息

{{ field[0] }}
{{ display(field[1]) }}

订单信息

{{ field[0] }}
{{ display(field[1]) }}

快递信息

{{ field[0] }}
{{ display(field[1]) }}
-

操作流水

展示创建、审核和编辑变更;四期不承载采购出库。
+

出库明细

共 {{ outboundDetails.length }} 条,出库量 {{ outboundDetails.reduce((sum, item) => sum + item.quantity, 0) }}
+

操作流水

展示创建、审核、入库与出库明细变更。
diff --git a/src/modules/overseas-business/pages/PurchaseOrderEditView.vue b/src/modules/overseas-business/pages/PurchaseOrderEditView.vue index b579d345..354c9014 100644 --- a/src/modules/overseas-business/pages/PurchaseOrderEditView.vue +++ b/src/modules/overseas-business/pages/PurchaseOrderEditView.vue @@ -33,10 +33,9 @@ const form = reactive(initial()) const rules: FormRules = { purchaseCategory: [{ required: true, message: '请输入采购品类', trigger: 'blur' }], productName: [{ required: true, message: '请输入商品名称', trigger: 'blur' }], - inboundQuantity: [{ required: true, type: 'number', min: 0, message: '入库量不能小于 0', trigger: 'change' }], } -const statusLabels: Record = { PENDING_REVIEW: '待审核', APPROVED: '已通过', REJECTED: '已拒绝' } -const inboundEditable = computed(() => form.status === 'APPROVED') +const statusLabels: Record = { PENDING_REVIEW: '待审核', APPROVED: '已通过', REJECTED: '已拒绝', PENDING_OUTBOUND: '待出库', PARTIALLY_OUTBOUND: '部分出库', OUTBOUNDED: '全部出库' } +const inboundEditable = computed(() => ['APPROVED', 'PENDING_OUTBOUND', 'PARTIALLY_OUTBOUND', 'OUTBOUNDED'].includes(form.status)) const outboundQuantity = computed(() => form.outboundDetails.reduce((sum, item) => sum + (Number(item.quantity) || 0), 0)) function addOutbound() { form.outboundDetails.push({ id: `new-${Date.now()}-${form.outboundDetails.length}`, quantity: 0, carrier: null, trackingNo: null, shippedAt: null, remark: null }) } function removeOutbound(index: number) { form.outboundDetails.splice(index, 1) } @@ -53,7 +52,7 @@ async function load() { loading.value = true try { const detail = await getOverseasPurchase(purchaseId.value, customerId.value) - Object.assign(form, { orderNo: detail.orderNo, purchaseCategory: detail.purchaseCategory, productName: detail.productName, productSpecification: detail.productSpecification, productUrl: detail.productUrl, inboundQuantity: detail.inboundQuantity, status: detail.status, purchaseWarehouseId: detail.purchaseWarehouseId, domesticCarrier: detail.domesticCarrier, domesticTrackingNo: detail.domesticTrackingNo, overseasCarrier: detail.overseasCarrier, overseasTrackingNo: detail.overseasTrackingNo, estimatedArrivalAt: detail.estimatedArrivalAt, remark: detail.remark, version: detail.version, outboundDetails: detail.outboundDetails.map((item) => ({ ...item })) }) + Object.assign(form, { orderNo: detail.orderNo, purchaseCategory: detail.purchaseCategory, productName: detail.productName, productSpecification: detail.productSpecification, productUrl: detail.productUrl, inboundQuantity: detail.inboundQuantity, status: detail.status, purchaseWarehouseId: detail.purchaseWarehouseId, domesticCarrier: detail.domesticCarrier, domesticTrackingNo: detail.domesticTrackingNo, overseasCarrier: detail.overseasCarrier, overseasTrackingNo: detail.overseasTrackingNo, estimatedArrivalAt: detail.estimatedArrivalAt, remark: detail.remark, version: detail.version, outboundDetails: (detail.outboundDetails || []).map((item) => ({ ...item })) }) if (detail.purchaseWarehouseId && !warehouses.value.some((item) => item.warehouseId === detail.purchaseWarehouseId)) warehouses.value.push({ warehouseId: detail.purchaseWarehouseId, warehouseCode: '', warehouseName: detail.purchaseWarehouseName || '当前采购仓', countryCode: '' }) captureBaseline() } catch (error) { loadError.value = getApiErrorDetails(error, '海外采购订单加载失败') } finally { loading.value = false } @@ -78,7 +77,7 @@ watch([purchaseId, customerId], () => { void loadWarehouses().then(load) }, { im
-
服务端仅允许已通过订单修改入库量{{ warehousesError }}
+
服务端仅允许已通过订单修改入库量{{ warehousesError }}
出库总量:{{ outboundQuantity }} / {{ form.inboundQuantity }}添加出库明细
暂无出库明细,可添加多条出库记录
保存取消
diff --git a/src/modules/overseas-business/pages/PurchaseOrdersView.vue b/src/modules/overseas-business/pages/PurchaseOrdersView.vue index a7632732..481a71bd 100644 --- a/src/modules/overseas-business/pages/PurchaseOrdersView.vue +++ b/src/modules/overseas-business/pages/PurchaseOrdersView.vue @@ -35,11 +35,12 @@ const columns: DataTableColumn[] = [ { key: 'productSpecification', prop: 'productSpecification', label: '商品规格', minWidth: 180, formatter: (row: OverseasPurchaseSummary) => row.productSpecification || '—' }, { key: 'budgetUnitPrice', label: '采购单价', width: 120, align: 'right', formatter: (row: OverseasPurchaseSummary) => formatAmount(row.budgetUnitPrice, row.currency) }, { key: 'inboundQuantity', prop: 'inboundQuantity', label: '入库量', width: 95, align: 'right' }, + { key: 'outboundQuantity', prop: 'outboundQuantity', label: '出库量', width: 95, align: 'right' }, { key: 'inventoryQuantity', prop: 'inventoryQuantity', label: '库存量', width: 95, align: 'right' }, { key: 'status', label: '状态', width: 105, formatter: (row: OverseasPurchaseSummary) => purchaseStatusLabel(row.status) }, { key: 'createdAt', prop: 'createdAt', label: '创建时间', minWidth: 165, formatter: (row: OverseasPurchaseSummary) => formatDateTime(row.createdAt) }, ] -const statusLabels: Record = { PENDING_REVIEW: '待审核', APPROVED: '已通过', REJECTED: '已拒绝' } +const statusLabels: Record = { PENDING_REVIEW: '待审核', APPROVED: '已通过', REJECTED: '已拒绝', PENDING_OUTBOUND: '待出库', PARTIALLY_OUTBOUND: '部分出库', OUTBOUNDED: '全部出库' } function purchaseStatusLabel(status: OverseasPurchaseStatus) { return statusLabels[status] || status } function formatAmount(value: string | null, currency: string) { if (value == null) return '—'; const amount = Number(value); return `${Number.isFinite(amount) ? amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : value} ${currency}` } function formatDateTime(value?: string | null) { return value ? value.replace('T', ' ').slice(0, 16) : '—' } diff --git a/src/modules/overseas-business/types.ts b/src/modules/overseas-business/types.ts index c83371e1..31faa715 100644 --- a/src/modules/overseas-business/types.ts +++ b/src/modules/overseas-business/types.ts @@ -5,7 +5,7 @@ export interface OverseasBusinessHistory { operatorId: string | null; changeDetail?: string | null; remark: string | null; createdAt: string } -export type OverseasPurchaseStatus = 'PENDING_REVIEW' | 'APPROVED' | 'REJECTED' +export type OverseasPurchaseStatus = 'PENDING_REVIEW' | 'APPROVED' | 'REJECTED' | 'PENDING_OUTBOUND' | 'PARTIALLY_OUTBOUND' | 'OUTBOUNDED' export interface OverseasPurchaseQuery { page?: number; size?: number; customerId?: string; orderNo?: string; customerKeyword?: string purchaseCategory?: string; productName?: string; status?: OverseasPurchaseStatus | '' @@ -16,7 +16,7 @@ export interface OverseasPurchaseSummary { purchaseCategory: string; productName: string; productSpecification: string | null purchaseQuantity: number; budgetUnitPrice: string | null; currency: string purchaseWarehouseId: string | null; purchaseWarehouseName: string | null - inboundQuantity: number; inventoryQuantity: number + inboundQuantity: number; outboundQuantity: number; inventoryQuantity: number status: OverseasPurchaseStatus; version: number; createdAt: string; updatedAt: string } export interface OverseasPurchaseDetail extends OverseasPurchaseSummary { @@ -24,7 +24,7 @@ export interface OverseasPurchaseDetail extends OverseasPurchaseSummary { domesticCarrier: string | null; domesticTrackingNo: string | null; overseasCarrier: string | null; overseasTrackingNo: string | null estimatedArrivalAt: string | null; remark: string | null; createdBy: string | null; reviewedBy: string | null reviewRemark: string | null; reviewedAt: string | null; histories: OverseasBusinessHistory[] - outboundDetails: OverseasPurchaseOutboundDetail[] + outboundDetails?: OverseasPurchaseOutboundDetail[] } export interface OverseasPurchaseOutboundDetail { id: string; quantity: number; carrier: string | null; trackingNo: string | null; shippedAt: string | null; remark: string | null } export interface OverseasPurchaseCustomerOption { customerId: string; customerName: string } diff --git a/src/modules/overseas-warehouse/api/deliveryOrders.test.ts b/src/modules/overseas-warehouse/api/deliveryOrders.test.ts index 86892b2a..fc27b3ce 100644 --- a/src/modules/overseas-warehouse/api/deliveryOrders.test.ts +++ b/src/modules/overseas-warehouse/api/deliveryOrders.test.ts @@ -17,6 +17,6 @@ describe('delivery order API', () => { it('loads an encoded detail in its explicit customer scope', async () => { vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', data: { order: { deliveryNo: 'DEL/1' }, skus: [], fees: [], operations: [] } }) await getDeliveryOrder('DEL/1', '1001') - expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/delivery-orders/DEL%2F1', { params: { customerId: '1001' } }) + expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/delivery-orders/DEL%2F1', { params: { customerId: '1001', page: 1, size: 200 } }) }) }) diff --git a/src/modules/overseas-warehouse/api/deliveryOrders.ts b/src/modules/overseas-warehouse/api/deliveryOrders.ts index 6c11f8be..45c1101e 100644 --- a/src/modules/overseas-warehouse/api/deliveryOrders.ts +++ b/src/modules/overseas-warehouse/api/deliveryOrders.ts @@ -68,7 +68,7 @@ export async function listDeliveryOrders(query: DeliveryOrderQuery) { export async function getDeliveryOrder(deliveryNo: string, customerId: string) { const response = await overseasHttp.get, ApiEnvelope>( - `/platform/v1/last-leg/delivery-orders/${encodeURIComponent(deliveryNo)}`, { params: { customerId } }, + `/platform/v1/last-leg/delivery-orders/${encodeURIComponent(deliveryNo)}`, { params: { customerId, page: 1, size: 200 } }, ) return unwrapApiResponse(response) } diff --git a/src/modules/overseas-warehouse/pages/DeliveryOrderDetailView.vue b/src/modules/overseas-warehouse/pages/DeliveryOrderDetailView.vue index c7478193..64982697 100644 --- a/src/modules/overseas-warehouse/pages/DeliveryOrderDetailView.vue +++ b/src/modules/overseas-warehouse/pages/DeliveryOrderDetailView.vue @@ -1,8 +1,9 @@ - - + + + +