diff --git a/src/app/components/AdminSidebarMenu.test.ts b/src/app/components/AdminSidebarMenu.test.ts index 5abcf329..ab1814d6 100644 --- a/src/app/components/AdminSidebarMenu.test.ts +++ b/src/app/components/AdminSidebarMenu.test.ts @@ -49,7 +49,7 @@ describe('AdminSidebarMenu', () => { expect(groupTitles).toContain('系统管理') expect(groupTitles).not.toContain('财务管理') expect(paths).toContain('/dashboard') - expect(paths).toContain('/inbounds') + expect(paths).toContain('/orders') expect(paths).toContain('/master-data/express-companies') expect(paths).not.toContain('/orders/first-leg') expect(paths).not.toContain('/first-leg-pricing/customer-rules') diff --git a/src/app/router/index.ts b/src/app/router/index.ts index 69220f0d..cc6a8481 100644 --- a/src/app/router/index.ts +++ b/src/app/router/index.ts @@ -6,6 +6,7 @@ import { useAccessPreviewStore } from '@/core/access/accessPreview' import { downloadRoutes } from '@/modules/downloads/routes' import { firstLegOrderRoutes } from '@/modules/first-leg-orders/routes' import { inboundRoutes } from '@/modules/inbounds/routes' +import { unifiedOrderRoutes } from '@/modules/unified-orders/routes' import { fbaPricingRoutes } from '@/modules/fba-pricing/routes' import { iamRoutes } from '@/modules/iam/routes' import { masterDataRoutes } from '@/modules/master-data/routes' @@ -43,6 +44,7 @@ const router = createRouter({ ...prototypeModuleRoutes, ...customerRoutes, ...warehouseRoutes, + ...unifiedOrderRoutes, ...firstLegOrderRoutes, ...inboundRoutes, ...fbaPricingRoutes, diff --git a/src/core/navigation/adminNavigation.test.ts b/src/core/navigation/adminNavigation.test.ts index 4fb26972..593b8b6e 100644 --- a/src/core/navigation/adminNavigation.test.ts +++ b/src/core/navigation/adminNavigation.test.ts @@ -29,8 +29,8 @@ describe('adminNavigation', () => { '海外仓阶梯费用', ]) expect(adminNavigation.find((item) => item.key === 'domestic-warehouse')?.children?.map((item) => item.title)).toEqual([ - '入库单列表', - '入库单测量', + '订单列表', + '订单测量', '测量数据审核', '库位管理', '托盘管理', @@ -73,7 +73,9 @@ describe('adminNavigation', () => { const permissions = leaves.flatMap((item) => item.permission ? [item.permission] : []) expect(paths).toContain('/dashboard') + expect(paths).toContain('/orders') expect(paths).not.toContain('/orders/first-leg') + expect(paths).not.toContain('/inbounds') expect(paths).not.toContain('/first-leg-pricing/customer-rules') expect(paths).toContain('/downloads') expect(paths).toContain('/prototype/finance/profit-report') @@ -110,7 +112,7 @@ describe('adminNavigation', () => { expect(filtered.map((item) => item.title)).toEqual(['首页数据看板', '国内仓', '供应商中心', '通用']) expect(paths).toEqual([ '/dashboard', - '/inbounds', + '/orders', '/master-data/express-companies', '/master-data/counterparties', '/warehouses', diff --git a/src/core/navigation/adminNavigation.ts b/src/core/navigation/adminNavigation.ts index 8c26a5ba..bd78b3d6 100644 --- a/src/core/navigation/adminNavigation.ts +++ b/src/core/navigation/adminNavigation.ts @@ -53,8 +53,8 @@ export const adminNavigation: AdminNavigationNode[] = [ icon: WarehouseIcon, iconColor: '#52c41a', children: [ - { key: 'inbound-list', title: '入库单列表', icon: ClipboardList, path: '/inbounds', permission: 'platform:firstleg:inbound:list' }, - { key: 'inbound-dedup', title: '入库单测量', icon: ClipboardList, path: '/prototype/domestic-warehouse/inbound-dedup', hidden: true }, + { key: 'order-list', title: '订单列表', icon: ClipboardList, path: '/orders', permission: 'platform:order:list', permissionAnyOf: ['platform:firstleg:inbound:list'] }, + { key: 'inbound-dedup', title: '订单测量', icon: ClipboardList, path: '/prototype/domestic-warehouse/inbound-dedup', hidden: true }, { key: 'inbound-measure', title: '测量数据审核', icon: FileCheck2, path: '/prototype/domestic-warehouse/inbound-measure', hidden: true }, { key: 'locations', title: '库位管理', icon: WarehouseIcon, path: '/master-data/locations', permission: 'platform:location:list' }, { key: 'pallets', title: '托盘管理', icon: WarehouseIcon, path: '/master-data/pallets', permission: 'platform:pallet:list' }, diff --git a/src/modules/fba-pricing/carrier-rates/pages/CarrierRatesView.vue b/src/modules/fba-pricing/carrier-rates/pages/CarrierRatesView.vue index a4d033e4..ed2e4694 100644 --- a/src/modules/fba-pricing/carrier-rates/pages/CarrierRatesView.vue +++ b/src/modules/fba-pricing/carrier-rates/pages/CarrierRatesView.vue @@ -275,7 +275,7 @@ async function loadCustomerMasterData(customerId: string) { masterDataLoading.value = true try { const [warehouseResult, partnerResult] = await Promise.all([ - listPlatformWarehouses(customerId, 1, 200), + listPlatformWarehouses(1, 200), listPlatformPartners(customerId, { page: 1, size: 200, type: 'CARRIER', status: 'ACTIVE' }), ]) if (sequence !== masterDataSequence) return false diff --git a/src/modules/first-leg-orders/routes.ts b/src/modules/first-leg-orders/routes.ts index 4b66e787..5a206499 100644 --- a/src/modules/first-leg-orders/routes.ts +++ b/src/modules/first-leg-orders/routes.ts @@ -4,7 +4,7 @@ export const firstLegOrderRoutes = [ { path: 'orders/first-leg', name: 'first-leg-orders', - component: () => import('@/modules/first-leg-orders/pages/FirstLegOrdersView.vue'), - meta: { title: '头程订单', description: '创建、受理和跟进客户提交的头程运输订单', permission: 'platform:order:list' }, + redirect: '/orders', + meta: { title: '订单列表', description: '旧头程订单入口已合并到统一订单列表', permission: 'platform:order:list' }, }, ] satisfies RouteRecordRaw[] diff --git a/src/modules/inbounds/components/InboundWorkflowWorkspace.test.ts b/src/modules/inbounds/components/InboundWorkflowWorkspace.test.ts index c0a236af..f3afd751 100644 --- a/src/modules/inbounds/components/InboundWorkflowWorkspace.test.ts +++ b/src/modules/inbounds/components/InboundWorkflowWorkspace.test.ts @@ -17,12 +17,12 @@ describe('InboundWorkflowWorkspace', () => { expect(source).toContain('label="基本信息"') expect(source).toContain('label="货物与测量"') expect(source).toContain('label="测量审核"') - expect(source).toContain('label="费用快照"') - expect(source).toContain('label="托盘与记录"') + expect(source).toContain('label="费用信息"') + expect(source).toContain('label="状态与其他"') }) it('uses the established first-leg detail density without nested framed surfaces', () => { - expect(source).toContain('padding: 20px 24px') + expect(source).toContain('padding: 18px 24px 16px') expect(source).toContain('border-bottom: 1px solid var(--cc-border)') expect(source).not.toContain('border-radius: 10px') expect(source).not.toContain(' { it('keeps pallet and location results read-only in the overview', () => { expect(source).toContain(':pallet-locations="detail.palletLocations"') - expect(source).toContain('label="托盘与记录"') + expect(source).toContain('label="状态与其他"') expect(source).not.toContain('reserveLocation') expect(source).not.toContain('allocatePallet') }) diff --git a/src/modules/inbounds/components/InboundWorkflowWorkspace.vue b/src/modules/inbounds/components/InboundWorkflowWorkspace.vue index 8917c6c6..713fb1d2 100644 --- a/src/modules/inbounds/components/InboundWorkflowWorkspace.vue +++ b/src/modules/inbounds/components/InboundWorkflowWorkspace.vue @@ -106,7 +106,7 @@ onBeforeUnmount(() => {
-

入仓码:{{ title }}

+

入库码:{{ title }}

按基本信息、货物测量、审核、费用和托盘库位顺序处理,所有结果以服务端最新状态为准。

@@ -134,7 +134,7 @@ onBeforeUnmount(() => { - +
@@ -169,7 +169,7 @@ onBeforeUnmount(() => { show-icon /> - + { @changed="handleDomainChanged" /> - + @@ -192,15 +192,18 @@ onBeforeUnmount(() => { diff --git a/src/modules/inbounds/core/InboundSummaryPanel.test.ts b/src/modules/inbounds/core/InboundSummaryPanel.test.ts index 090e40b0..f66f3917 100644 --- a/src/modules/inbounds/core/InboundSummaryPanel.test.ts +++ b/src/modules/inbounds/core/InboundSummaryPanel.test.ts @@ -14,6 +14,9 @@ describe('InboundSummaryPanel', () => { expect(source).toContain('summary.chargeableWeightKg') expect(source).toContain('summary.locationCodes') expect(source).toContain('summary.notificationStatus') + expect(source).toContain('publicRemark') + expect(source).toContain('买单 / 退税') + expect(source).toContain('计费毛重') }) it('does not restore excluded receiving review or customer fee confirmation actions', () => { diff --git a/src/modules/inbounds/core/InboundSummaryPanel.vue b/src/modules/inbounds/core/InboundSummaryPanel.vue index 6b7483e4..899103cd 100644 --- a/src/modules/inbounds/core/InboundSummaryPanel.vue +++ b/src/modules/inbounds/core/InboundSummaryPanel.vue @@ -2,7 +2,7 @@ import { Box, MapPin, Scale, Warehouse } from '@lucide/vue' import type { InboundOrderSummary } from './types' -defineProps<{ summary: InboundOrderSummary }>() +withDefaults(defineProps<{ summary: InboundOrderSummary; publicRemark?: string | null }>(), { publicRemark: null }) const processLabels: Record = { WAIT_MEASURE: '待测量', MEASURING: '测量中', MEASURE_PENDING: '待审核', MEASURE_REJECTED: '测量驳回', @@ -33,8 +33,9 @@ function label(value: string | null, labels: Record) { return va diff --git a/src/modules/inbounds/routes.ts b/src/modules/inbounds/routes.ts index bf75c74b..c509688e 100644 --- a/src/modules/inbounds/routes.ts +++ b/src/modules/inbounds/routes.ts @@ -4,10 +4,10 @@ export const inboundRoutes = [ { path: 'inbounds', name: 'inbound-orders', - component: () => import('./pages/InboundOrdersView.vue'), + redirect: '/orders', meta: { - title: '入库单', - description: '查询头程入库单并跟进测量、审核、费用和入库进度', + title: '订单列表', + description: '旧入库列表入口已合并到统一订单列表', permission: 'platform:firstleg:inbound:list', }, }, diff --git a/src/modules/unified-orders/api/unifiedOrders.test.ts b/src/modules/unified-orders/api/unifiedOrders.test.ts new file mode 100644 index 00000000..5889b059 --- /dev/null +++ b/src/modules/unified-orders/api/unifiedOrders.test.ts @@ -0,0 +1,52 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { http } from '@/shared/api/http' +import { getPlatformUnifiedOrder, listPlatformUnifiedOrders } from './unifiedOrders' + +vi.mock('@/shared/api/http', () => ({ + http: { get: vi.fn() }, + unwrapApiResponse: (response: { data: T }) => response.data, +})) + +describe('unified orders API contract', () => { + beforeEach(() => vi.mocked(http.get).mockReset()) + + it('uses the C02 platform orders endpoint and contract filters', async () => { + vi.mocked(http.get).mockResolvedValue({ data: { records: [], page: '2', size: '20', total: '0', pages: '0' } }) + await expect(listPlatformUnifiedOrders({ + keyword: 'CC', orderNo: 'CC-1', mainStatus: 'WAIT_SPLIT', customerId: '30001', + receivingWarehouseId: '11', destinationWarehouseId: '22', page: 2, size: 20, + })).resolves.toMatchObject({ page: 2, size: 20, total: 0, pages: 0 }) + expect(http.get).toHaveBeenCalledWith('/platform/v1/orders', { + params: { + keyword: 'CC', orderNo: 'CC-1', mainStatus: 'WAIT_SPLIT', customerId: '30001', + receivingWarehouseId: '11', destinationWarehouseId: '22', page: 2, size: 20, + }, + }) + }) + + it('normalizes C02 row IDs and keeps inboundId for measurement routing', async () => { + vi.mocked(http.get).mockResolvedValue({ data: { + records: [{ orderId: '90001', tenantId: '208', inboundId: '77', inboundVersion: 3, mainStatus: 'WAIT_MEASURE', measurement: null, preload: null, loading: null, delivery: null, transport: null }], + page: 1, size: 20, total: 1, pages: 1, + } }) + await expect(listPlatformUnifiedOrders({ page: 1, size: 20 })).resolves.toMatchObject({ + records: [{ orderId: '90001', tenantId: '208', inboundId: '77', inboundVersion: 3 }], + }) + }) + + it('loads the C02 detail shape and passes tenantId only on the detail boundary', async () => { + vi.mocked(http.get).mockResolvedValue({ data: { + summary: { orderId: '90001', tenantId: '208', mainStatus: 'WAIT_SPLIT' }, + order: { order: {}, cargoes: [], parties: [], internalNotes: [], statusHistory: [] }, + timeline: [], + } }) + await expect(getPlatformUnifiedOrder('208', '90001')).resolves.toMatchObject({ + summary: { orderId: '90001' }, order: { internalNotes: [] }, timeline: [], + }) + expect(http.get).toHaveBeenCalledWith('/platform/v1/orders/90001', { params: { tenantId: '208' } }) + }) + + it('does not expose a guessed generic action endpoint', async () => { + expect(http).not.toHaveProperty('post') + }) +}) diff --git a/src/modules/unified-orders/api/unifiedOrders.ts b/src/modules/unified-orders/api/unifiedOrders.ts new file mode 100644 index 00000000..026d4d05 --- /dev/null +++ b/src/modules/unified-orders/api/unifiedOrders.ts @@ -0,0 +1,264 @@ +import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' + +export const unifiedOrdersApiPath = '/platform/v1/orders' + +export type UnifiedOrderMainStatus = + | 'PENDING_REVIEW' + | 'WAIT_MEASURE' + | 'INBOUNDED' + | 'WAIT_SPLIT' + | 'LOADING' + | 'LOADED' + | 'DELIVERED' + +export const unifiedOrderStatusOptions: Array<{ value: UnifiedOrderMainStatus; label: string }> = [ + { value: 'PENDING_REVIEW', label: '待审核' }, + { value: 'WAIT_MEASURE', label: '待测量数据' }, + { value: 'INBOUNDED', label: '已入库' }, + { value: 'WAIT_SPLIT', label: '待分柜' }, + { value: 'LOADING', label: '装柜中' }, + { value: 'LOADED', label: '装柜完成' }, + { value: 'DELIVERED', label: '派送完成' }, +] + +export interface UnifiedOrderMeasurementSummary { + reviewStatus: string | null + actualGrossWeightKg: number | null + actualVolumeCbm: number | null + chargeableWeightKg: number | null + completedAt: string | null +} + +export interface UnifiedOrderPreloadSummary { + status: string | null + version: number | null + itemCount: number | null + updatedAt: string | null +} + +export interface UnifiedOrderLoadingSummary { + status: string | null + loadingVersion: number | null + pickingVersion: number | null + expectedCount: number | null + pickedCount: number | null + evidenceCount: number | null + sealNo: string | null + updatedAt: string | null +} + +export interface UnifiedOrderDeliverySummary { + status: string | null + completedAt: string | null + trackingNo: string | null +} + +export interface UnifiedOrderTransportSummary { + loadingStatus: string | null + containerNo: string | null + cabinetNo: string | null + vesselName: string | null + voyageNo: string | null + sailingAt: string | null + portArrivalAt: string | null +} + +export interface UnifiedOrderResource { + orderId: number | string + tenantId: number | string + orderNo: string | null + mainStatus: UnifiedOrderMainStatus + nextAction: string | null + internalOrderStatus: string | null + customerId: number | string | null + customerName: string | null + channelName: string | null + receivingWarehouseId: number | string | null + receivingWarehouseName: string | null + destinationWarehouseId: number | string | null + destinationWarehouseName: string | null + inboundId: number | string | null + inboundVersion: number | null + inboundCode: string | null + expectedShipDate: string | null + totalPackageCount: number | null + measurement: UnifiedOrderMeasurementSummary | null + preload: UnifiedOrderPreloadSummary | null + loading: UnifiedOrderLoadingSummary | null + delivery: UnifiedOrderDeliverySummary | null + transport: UnifiedOrderTransportSummary | null + createdAt: string | null + updatedAt: string | null +} + +export interface UnifiedOrderTimelineEvent { + source: string | null + eventType: string | null + mainStatus: UnifiedOrderMainStatus | null + action: string | null + remark: string | null + operatorId: number | string | null + operatorName: string | null + occurredAt: string | null +} + +export interface UnifiedOrderDetailOrderSnapshot { + id: number | string | null + tenantId: number | string | null + orderNo: string | null + customerId: number | string | null + customerName: string | null + transportMode: string | null + destinationCountryCode: string | null + destinationWarehouseId: number | string | null + destinationWarehouseName: string | null + expectedShipDate: string | null + receivingWarehouseId: number | string | null + receivingWarehouseName: string | null + channelName: string | null + createdAt: string | null + updatedAt: string | null +} + +export interface UnifiedOrderDetailOrderResource { + order: UnifiedOrderDetailOrderSnapshot | null + cargoes: Array> + parties: Array> + internalNotes: UnifiedOrderInternalNoteResource[] + statusHistory: UnifiedOrderStatusHistoryResource[] +} + +export interface UnifiedOrderInternalNoteResource { + id: number | string + content: string + createdBy: number | string | null + createdAt: string | null +} + +export interface UnifiedOrderStatusHistoryResource { + id: number | string + fromStatus: string | null + toStatus: string | null + action: string | null + reason: string | null + operatorId: number | string | null + operatorName: string | null + occurredAt: string | null +} + +export interface UnifiedOrderDetailResource { + summary: UnifiedOrderResource + order: UnifiedOrderDetailOrderResource + timeline: UnifiedOrderTimelineEvent[] +} + +export interface UnifiedOrderPageResource { + records: UnifiedOrderResource[] + page: number + size: number + total: number + pages: number +} + +export interface UnifiedOrderQuery { + tenantId?: string + keyword?: string + orderNo?: string + mainStatus?: UnifiedOrderMainStatus + customerId?: string + receivingWarehouseId?: string + destinationWarehouseId?: string + createdFrom?: string + createdTo?: string + page: number + size: number +} + +type PagePayload = Omit & { + page: number | string + size: number | string + total: number | string + pages: number | string +} + +function integer(value: number | string | null | undefined, fallback: number) { + const parsed = Number(value) + return Number.isFinite(parsed) && parsed >= 0 ? Math.trunc(parsed) : fallback +} + +function nullableInteger(value: number | string | null | undefined) { + if (value === null || value === undefined || value === '') return null + return integer(value, 0) +} + +function numberValue(value: number | string | null | undefined) { + if (value === null || value === undefined || value === '') return null + const parsed = Number(value) + return Number.isFinite(parsed) ? parsed : null +} + +function id(value: number | string | null | undefined) { + return value === null || value === undefined || value === '' ? null : String(value) +} + +function queryParams(query: UnifiedOrderQuery) { + return Object.fromEntries(Object.entries(query).filter(([, value]) => value !== undefined && value !== '')) +} + +function normalizeRow(raw: UnifiedOrderResource): UnifiedOrderResource { + return { + ...raw, + orderId: String(raw.orderId), + tenantId: String(raw.tenantId), + customerId: id(raw.customerId), + receivingWarehouseId: id(raw.receivingWarehouseId), + destinationWarehouseId: id(raw.destinationWarehouseId), + inboundId: id(raw.inboundId), + inboundVersion: nullableInteger(raw.inboundVersion), + totalPackageCount: nullableInteger(raw.totalPackageCount), + measurement: raw.measurement ? { + ...raw.measurement, + actualGrossWeightKg: numberValue(raw.measurement.actualGrossWeightKg), + actualVolumeCbm: numberValue(raw.measurement.actualVolumeCbm), + chargeableWeightKg: numberValue(raw.measurement.chargeableWeightKg), + } : null, + preload: raw.preload ? { ...raw.preload, version: nullableInteger(raw.preload.version), itemCount: nullableInteger(raw.preload.itemCount) } : null, + loading: raw.loading ? { + ...raw.loading, + loadingVersion: nullableInteger(raw.loading.loadingVersion), + pickingVersion: nullableInteger(raw.loading.pickingVersion), + expectedCount: nullableInteger(raw.loading.expectedCount), + pickedCount: nullableInteger(raw.loading.pickedCount), + evidenceCount: nullableInteger(raw.loading.evidenceCount), + } : null, + transport: raw.transport ? { ...raw.transport } : null, + } +} + +export async function listPlatformUnifiedOrders(query: UnifiedOrderQuery) { + const response = await http.get, ApiEnvelope>(unifiedOrdersApiPath, { + params: queryParams(query), + }) + const result = unwrapApiResponse(response) + return { + ...result, + records: (result.records || []).map(normalizeRow), + page: integer(result.page, query.page), + size: integer(result.size, query.size), + total: integer(result.total, 0), + pages: integer(result.pages, 0), + } satisfies UnifiedOrderPageResource +} + +export async function getPlatformUnifiedOrder(tenantId: string, orderId: string) { + const response = await http.get, ApiEnvelope>( + `${unifiedOrdersApiPath}/${orderId}`, + { params: { tenantId } }, + ) + const result = unwrapApiResponse(response) + return { ...result, summary: normalizeRow(result.summary) } +} + +export function unifiedOrderStatusLabel(status: UnifiedOrderMainStatus) { + return unifiedOrderStatusOptions.find((item) => item.value === status)?.label || status +} diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts new file mode 100644 index 00000000..150fc21a --- /dev/null +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts @@ -0,0 +1,51 @@ +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { describe, expect, it } from 'vitest' +import { unifiedOrderStatusOptions } from '../api/unifiedOrders' + +const source = readFileSync(resolve(process.cwd(), 'src/modules/unified-orders/pages/UnifiedOrdersView.vue'), 'utf8') + +describe('UnifiedOrdersView contract', () => { + it('only exposes the seven product main statuses', () => { + expect(unifiedOrderStatusOptions.map((item) => item.label)).toEqual([ + '待审核', '待测量数据', '已入库', '待分柜', '装柜中', '装柜完成', '派送完成', + ]) + for (const legacyLabel of ['待客户确认', '差异处理中', '派送中', '进入分柜']) { + expect(source).not.toContain(legacyLabel) + } + }) + + it('covers real list states and keeps operation column first and left-fixed', () => { + expect(source).toContain("{ key: 'actions', label: '操作', width: 180, fixed: 'left'") + expect(source).toContain('title="订单列表暂时无法加载"') + expect(source).toContain('empty-text="暂无符合条件的订单"') + expect(source).toContain(':loading="loading"') + expect(source).toContain('v-model:page="page"') + expect(source).toContain('v-model:page-size="pageSize"') + }) + + it('keeps business actions as explicit C01/C03/C04/C06 boundaries', () => { + expect(source).toContain('nextActionBoundary') + expect(source).toContain('待接入:${label}') + expect(source).toContain('CONFIRM_PRELOAD') + expect(source).toContain("can('platform:firstleg:inbound:view')") + expect(source).toContain('openMeasurement') + expect(source).not.toContain('updateMainStatus') + expect(source).not.toContain('人工进入分柜') + expect(source).not.toContain('performPlatformUnifiedOrderAction') + expect(source).not.toContain('/actions/') + }) + + it('provides unified detail, history and internal note states', () => { + expect(source).toContain('getPlatformUnifiedOrder') + expect(source).toContain('aria-label="状态历史"') + expect(source).toContain('aria-label="内部备注"') + expect(source).toContain('订单详情暂时无法加载') + }) + + it('exposes the C06 workflow entry only for inbound-backed downstream states', () => { + expect(source).toContain("name: 'unified-order-workflow'") + expect(source).toContain("['WAIT_SPLIT', 'LOADING', 'LOADED'].includes(row.mainStatus)") + expect(source).toContain('后半程作业') + }) +}) diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.vue b/src/modules/unified-orders/pages/UnifiedOrdersView.vue new file mode 100644 index 00000000..99bcb9c7 --- /dev/null +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.vue @@ -0,0 +1,327 @@ + + + + + diff --git a/src/modules/unified-orders/routes.ts b/src/modules/unified-orders/routes.ts new file mode 100644 index 00000000..1d8411f5 --- /dev/null +++ b/src/modules/unified-orders/routes.ts @@ -0,0 +1,37 @@ +import type { RouteRecordRaw } from 'vue-router' + +export const unifiedOrderRoutes = [ + { + path: 'orders', + name: 'unified-orders', + component: () => import('./pages/UnifiedOrdersView.vue'), + meta: { + title: '订单列表', + description: '围绕同一订单主线跟进审核、测量、分柜、装柜和派送', + permission: 'platform:order:list', + permissionAnyOf: ['platform:firstleg:inbound:list'], + }, + }, + { + path: 'orders/:orderId/workflow', + name: 'unified-order-workflow', + component: () => import('./workflows/pages/UnifiedOrderWorkflowView.vue'), + meta: { + title: '订单后半程作业', + description: '处理分柜、预装、装柜和派送作业', + permission: 'platform:firstleg:inbound:view', + permissionAnyOf: ['platform:firstleg:inbound:list', 'platform:order:list'], + }, + }, + { + path: 'orders/:orderId', + name: 'unified-order-detail', + component: () => import('./pages/UnifiedOrdersView.vue'), + meta: { + title: '订单详情', + description: '查看订单主状态、状态历史和平台备注', + permission: 'platform:order:view', + permissionAnyOf: ['platform:order:list', 'platform:firstleg:inbound:view'], + }, + }, +] satisfies RouteRecordRaw[] diff --git a/src/modules/unified-orders/workflows/README.md b/src/modules/unified-orders/workflows/README.md new file mode 100644 index 00000000..a588c46e --- /dev/null +++ b/src/modules/unified-orders/workflows/README.md @@ -0,0 +1,16 @@ +# C06 Admin 后半程作业 + +本目录只承载统一订单详情的后半程作业接入点,供 C05 统一订单列表在详情抽屉或作业页中装配。 + +真实接口均使用 `/api/platform/v1/inbounds/{inboundId}`: + +- `POST /split/smart`:智能分柜/保存预装清单 +- `GET /preload`、`POST /preload/confirming`、`POST /preload/confirm`:预装清单查询与确认 +- `GET/POST /loading/picking`、`/loading/picking/start`、`/loading/picking/complete`:拣货 +- `GET/POST /loading/evidence`:四步照片与封签证据元数据 +- `POST /loading/complete`:完成装柜 +- `POST /delivery/complete`:确认派送完成 + +所有写请求带 `Idempotency-Key` 和服务端要求的 `expectedVersion`。`loading/complete` 与 `delivery/complete` 使用入库单版本;拣货使用拣货任务版本。当前没有 File 服务上传接口,证据组件不会把本地路径伪装成已上传文件。 + +权限码、状态流转和请求字段以 Firstleg Controller/DTO 为准;本模块不新增“进入分柜”操作,也不定义通用 `/actions/{ACTION}` 接口。 diff --git a/src/modules/unified-orders/workflows/api.test.ts b/src/modules/unified-orders/workflows/api.test.ts new file mode 100644 index 00000000..cb45a30e --- /dev/null +++ b/src/modules/unified-orders/workflows/api.test.ts @@ -0,0 +1,70 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { http } from '@/shared/api/http' +import { + completeDelivery, + completeLoading, + confirmingPreload, + getPicking, + getPreload, + smartSplit, +} from './api' + +vi.mock('@/shared/api/http', () => ({ + http: { get: vi.fn(), post: vi.fn() }, + unwrapApiResponse: (response: { data: T }) => response.data, +})) + +describe('unified order workflow API contract', () => { + beforeEach(() => { + vi.mocked(http.get).mockReset() + vi.mocked(http.post).mockReset() + }) + + it('uses the C03 preload endpoints and expected version', async () => { + vi.mocked(http.get).mockResolvedValue({ data: { id: 1, inboundId: 8, tenantId: 2, status: 'PENDING', version: 3, items: [] } }) + vi.mocked(http.post).mockResolvedValue({ data: { mainStatus: 'WAIT_SPLIT', preloadPlan: null } }) + await getPreload('2', '8') + await smartSplit('2', '8', { expectedVersion: 3, items: [] }) + expect(http.get).toHaveBeenCalledWith('/platform/v1/inbounds/8/preload', { params: { tenantId: '2' } }) + expect(http.post).toHaveBeenCalledWith( + '/platform/v1/inbounds/8/split/smart', { expectedVersion: 3, items: [] }, + expect.objectContaining({ params: { tenantId: '2' }, headers: { 'Idempotency-Key': expect.any(String) } }), + ) + }) + + it('uses C04 picking and loading paths without a generic action endpoint', async () => { + vi.mocked(http.get).mockResolvedValue({ data: { id: 1, inboundId: 8, status: 'PICKING', version: 4 } }) + vi.mocked(http.post).mockResolvedValue({ data: { mainStatus: 'LOADED', evidences: null } }) + await getPicking('2', '8') + await completeLoading('2', '8', { expectedVersion: 7 }) + expect(http.get).toHaveBeenCalledWith('/platform/v1/inbounds/8/loading/picking', { params: { tenantId: '2' } }) + expect(http.post).toHaveBeenCalledWith( + '/platform/v1/inbounds/8/loading/complete', { expectedVersion: 7 }, + expect.objectContaining({ params: { tenantId: '2' }, headers: { 'Idempotency-Key': expect.any(String) } }), + ) + expect(vi.mocked(http.post).mock.calls.some(([url]) => String(url).includes('/actions/'))).toBe(false) + }) + + it('uses the inbound version for delivery completion', async () => { + vi.mocked(http.post).mockResolvedValue({ data: { mainStatus: 'DELIVERED', evidences: null } }) + await completeDelivery('2', '8', { expectedVersion: 9, remark: '已确认派送' }) + expect(http.post).toHaveBeenCalledWith( + '/platform/v1/inbounds/8/delivery/complete', { expectedVersion: 9, remark: '已确认派送' }, + expect.objectContaining({ params: { tenantId: '2' }, headers: { 'Idempotency-Key': expect.any(String) } }), + ) + }) + + it('does not swallow non-success responses', async () => { + vi.mocked(http.get).mockRejectedValue(new Error('无权限')) + await expect(getPreload('2', '8')).rejects.toThrow('无权限') + }) + + it('supports the intermediate confirming preload action', async () => { + vi.mocked(http.post).mockResolvedValue({ data: { mainStatus: 'WAIT_SPLIT', preloadPlan: null } }) + await confirmingPreload('2', '8', { expectedVersion: 1 }) + expect(http.post).toHaveBeenCalledWith( + '/platform/v1/inbounds/8/preload/confirming', { expectedVersion: 1 }, + expect.objectContaining({ params: { tenantId: '2' } }), + ) + }) +}) diff --git a/src/modules/unified-orders/workflows/api.ts b/src/modules/unified-orders/workflows/api.ts new file mode 100644 index 00000000..fbc4cf14 --- /dev/null +++ b/src/modules/unified-orders/workflows/api.ts @@ -0,0 +1,229 @@ +import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' + +const inboundBasePath = '/platform/v1/inbounds' + +export type PreloadStatus = 'PENDING' | 'CONFIRMING' | 'CONFIRMED' | string +export type EvidenceType = 'PHOTO_STEP_1' | 'PHOTO_STEP_2' | 'PHOTO_STEP_3' | 'PHOTO_STEP_4' | 'SEAL' + +export interface PreloadItem { + containerCode: string | null + cargoReference: string | null + packageCount: number | null + volumeCbm: number | null + remark: string | null +} + +export interface PreloadPlan { + id: number | string + inboundId: number | string + tenantId: number | string + status: PreloadStatus + version: number + items: PreloadItem[] + remark: string | null + createdAt: string | null + updatedAt: string | null +} + +export interface SplitActionResponse { + mainStatus: string + preloadPlan: PreloadPlan | null +} + +export interface PickingTask { + id: number | string + inboundId: number | string + status: string + version: number + expectedCount: number | null + pickedCount: number | null + remark: string | null +} + +export interface Evidence { + id: number | string + inboundId: number | string + tenantId: number | string + evidenceType: EvidenceType + fileId: number | string | null + fileName: string | null + sealNo: string | null + remark: string | null + version: number + createdAt: string | null + updatedAt: string | null +} + +export interface EvidenceList { + inboundId: number | string + evidences: Evidence[] +} + +export interface LoadingActionResponse { + mainStatus: string + evidences: EvidenceList | null +} + +export interface PickingRequest { + expectedVersion: number + expectedCount: number + pickedCount: number + remark?: string +} + +export interface SmartSplitRequest { + expectedVersion: number + items: PreloadItem[] + remark?: string +} + +export interface ConfirmPreloadRequest { + expectedVersion: number +} + +export interface UpsertEvidenceRequest { + expectedVersion: number + evidenceType: EvidenceType + fileId?: number | string + fileName?: string + sealNo?: string + remark?: string +} + +export interface CompleteLoadingRequest { + expectedVersion: number + remark?: string +} + +export interface CompleteDeliveryRequest { + expectedVersion: number + remark?: string +} + +function query(tenantId: string | number) { + return { tenantId: String(tenantId) } +} + +function idempotencyKey(operation: string, inboundId: string | number) { + return `admin-${operation}-${inboundId}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` +} + +function writeOptions(operation: string, inboundId: string | number, tenantId: string | number) { + return { params: query(tenantId), headers: { 'Idempotency-Key': idempotencyKey(operation, inboundId) } } +} + +export async function getPreload(tenantId: string | number, inboundId: string | number) { + const response = await http.get, ApiEnvelope>( + `${inboundBasePath}/${inboundId}/preload`, { params: query(tenantId) }, + ) + return unwrapApiResponse(response) +} + +export async function smartSplit( + tenantId: string | number, + inboundId: string | number, + request: SmartSplitRequest, +) { + const response = await http.post, ApiEnvelope>( + `${inboundBasePath}/${inboundId}/split/smart`, request, writeOptions('smart-split', inboundId, tenantId), + ) + return unwrapApiResponse(response) +} + +export async function confirmingPreload( + tenantId: string | number, + inboundId: string | number, + request: ConfirmPreloadRequest, +) { + const response = await http.post, ApiEnvelope>( + `${inboundBasePath}/${inboundId}/preload/confirming`, request, + writeOptions('preload-confirming', inboundId, tenantId), + ) + return unwrapApiResponse(response) +} + +export async function confirmPreload( + tenantId: string | number, + inboundId: string | number, + request: ConfirmPreloadRequest, +) { + const response = await http.post, ApiEnvelope>( + `${inboundBasePath}/${inboundId}/preload/confirm`, request, + writeOptions('preload-confirm', inboundId, tenantId), + ) + return unwrapApiResponse(response) +} + +export async function getPicking(tenantId: string | number, inboundId: string | number) { + const response = await http.get, ApiEnvelope>( + `${inboundBasePath}/${inboundId}/loading/picking`, { params: query(tenantId) }, + ) + return unwrapApiResponse(response) +} + +export async function startPicking( + tenantId: string | number, + inboundId: string | number, + request: PickingRequest, +) { + const response = await http.post, ApiEnvelope>( + `${inboundBasePath}/${inboundId}/loading/picking/start`, request, + writeOptions('picking-start', inboundId, tenantId), + ) + return unwrapApiResponse(response) +} + +export async function completePicking( + tenantId: string | number, + inboundId: string | number, + request: PickingRequest, +) { + const response = await http.post, ApiEnvelope>( + `${inboundBasePath}/${inboundId}/loading/picking/complete`, request, + writeOptions('picking-complete', inboundId, tenantId), + ) + return unwrapApiResponse(response) +} + +export async function getEvidence(tenantId: string | number, inboundId: string | number) { + const response = await http.get, ApiEnvelope>( + `${inboundBasePath}/${inboundId}/loading/evidence`, { params: query(tenantId) }, + ) + return unwrapApiResponse(response) +} + +export async function upsertEvidence( + tenantId: string | number, + inboundId: string | number, + request: UpsertEvidenceRequest, +) { + const response = await http.post, ApiEnvelope>( + `${inboundBasePath}/${inboundId}/loading/evidence`, request, + writeOptions(`evidence-${request.evidenceType}`, inboundId, tenantId), + ) + return unwrapApiResponse(response) +} + +export async function completeLoading( + tenantId: string | number, + inboundId: string | number, + request: CompleteLoadingRequest, +) { + const response = await http.post, ApiEnvelope>( + `${inboundBasePath}/${inboundId}/loading/complete`, request, + writeOptions('loading-complete', inboundId, tenantId), + ) + return unwrapApiResponse(response) +} + +export async function completeDelivery( + tenantId: string | number, + inboundId: string | number, + request: CompleteDeliveryRequest, +) { + const response = await http.post, ApiEnvelope>( + `${inboundBasePath}/${inboundId}/delivery/complete`, request, + writeOptions('delivery-complete', inboundId, tenantId), + ) + return unwrapApiResponse(response) +} diff --git a/src/modules/unified-orders/workflows/components/UnifiedOrderWorkflowPanel.vue b/src/modules/unified-orders/workflows/components/UnifiedOrderWorkflowPanel.vue new file mode 100644 index 00000000..de3226ad --- /dev/null +++ b/src/modules/unified-orders/workflows/components/UnifiedOrderWorkflowPanel.vue @@ -0,0 +1,388 @@ + + + + + diff --git a/src/modules/unified-orders/workflows/index.ts b/src/modules/unified-orders/workflows/index.ts new file mode 100644 index 00000000..91c3317e --- /dev/null +++ b/src/modules/unified-orders/workflows/index.ts @@ -0,0 +1,3 @@ +export { default as UnifiedOrderWorkflowPanel } from './components/UnifiedOrderWorkflowPanel.vue' +export { default as UnifiedOrderWorkflowView } from './pages/UnifiedOrderWorkflowView.vue' +export * from './api' diff --git a/src/modules/unified-orders/workflows/pages/UnifiedOrderWorkflowView.vue b/src/modules/unified-orders/workflows/pages/UnifiedOrderWorkflowView.vue new file mode 100644 index 00000000..4b6c4df9 --- /dev/null +++ b/src/modules/unified-orders/workflows/pages/UnifiedOrderWorkflowView.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/src/modules/warehouse-resources/locations/pages/LocationsView.test.ts b/src/modules/warehouse-resources/locations/pages/LocationsView.test.ts index 131ecf67..5216272f 100644 --- a/src/modules/warehouse-resources/locations/pages/LocationsView.test.ts +++ b/src/modules/warehouse-resources/locations/pages/LocationsView.test.ts @@ -9,13 +9,11 @@ import { listPlatformLocations, type PlatformLocationResource, } from '../api/platformLocations' -import { listPlatformTenants, type PlatformTenantResource } from '../../../iam/api/platformIam' import { listPlatformWarehouses, type PlatformWarehouseResource } from '../../../warehouses/api/platformWarehouses' import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue' import DataTable from '@/shared/components/DataTable.vue' import LocationsView from './LocationsView.vue' -vi.mock('../../../iam/api/platformIam', () => ({ listPlatformTenants: vi.fn() })) vi.mock('../../../warehouses/api/platformWarehouses', () => ({ listPlatformWarehouses: vi.fn() })) vi.mock('../api/platformLocations', () => ({ listPlatformLocations: vi.fn(), @@ -44,32 +42,7 @@ function createMemoryStorage(): Storage { } } -const tenant = (id: number | string): PlatformTenantResource => ({ - id, - tenantCode: String(id), - tenantName: '雪花企业', - status: 'ACTIVE', - legalName: '雪花企业', - legalRepresentative: null, - unifiedSocialCreditCode: null, - contactName: null, - contactPhone: null, - contactEmail: null, - servicePackage: null, - contactWechat: null, - salesOwnerName: null, - paymentTermDays: null, - enterpriseType: null, - establishedDate: null, - registeredCapital: null, - registeredCapitalCurrency: null, - registeredAddress: null, - internalRemark: null, - createdAt: null, - approvedAt: null, -}) - -const tenantId = '9223372036854775806' +const tenantId = '0' const warehouseId = '9223372036854775805' const warehouse: PlatformWarehouseResource = { id: warehouseId, @@ -151,9 +124,6 @@ describe('LocationsView', () => { beforeEach(() => { vi.stubGlobal('ResizeObserver', ResizeObserverStub) document.body.innerHTML = '' - vi.mocked(listPlatformTenants).mockReset().mockResolvedValue({ - records: [tenant(tenantId)], page: 1, size: 200, total: 1, pages: 1, - }) vi.mocked(listPlatformWarehouses).mockReset().mockResolvedValue({ records: [warehouse], page: 1, size: 200, total: 1, pages: 1, }) @@ -170,14 +140,13 @@ describe('LocationsView', () => { const toolbar = wrapper.findComponent(QueryToolbar) const fields = toolbar.props('fields') as QueryField[] - expect(listPlatformTenants).toHaveBeenCalledWith({ page: 1, size: 200 }) expect(wrapper.find('[aria-label="所属企业"]').exists()).toBe(false) expect(fields[0]?.placeholder).toBe('全部仓库') expect(fields.map(({ key, width }) => [key, width])).toEqual([ ['warehouseId', 300], ['zoneCode', 210], ['locationCode', 220], ['locationType', 170], ['status', 160], ]) - expect(listPlatformWarehouses).toHaveBeenCalledWith(undefined, 1, 200) + expect(listPlatformWarehouses).toHaveBeenCalledWith(1, 200) expect(listPlatformLocations).toHaveBeenCalledWith({ page: 1, size: 20, @@ -206,7 +175,7 @@ describe('LocationsView', () => { expect(panelHeading.text()).toContain('共 2 个库位') expect(panelHeading.text()).toContain('新增库位') expect(wrapper.findComponent(QueryToolbar).text()).not.toContain('新增库位') - expect(listPlatformWarehouses).toHaveBeenCalledWith(undefined, 1, 200) + expect(listPlatformWarehouses).toHaveBeenCalledWith(1, 200) expect(listPlatformLocations).toHaveBeenLastCalledWith({ page: 1, size: 20, @@ -259,7 +228,8 @@ describe('LocationsView', () => { expect(getPlatformLocation).toHaveBeenCalledWith(tenantId, warehouseId, availableLocation.id) expect(document.body.textContent).toContain('库位详情') expect(document.body.textContent).toContain('义乌收货仓(CN-YW-01)') - expect(document.body.textContent).toContain(`所属企业雪花企业(${tenantId})`) + expect(document.body.textContent).toContain('资源作用域平台公共资源') + expect(document.body.textContent).not.toContain('所属企业') expect(document.body.textContent).not.toContain('企业 ID') expect(document.body.textContent).toContain('库位名称A1 区 01 位') expect(document.body.textContent).toContain('v3') diff --git a/src/modules/warehouse-resources/locations/pages/LocationsView.vue b/src/modules/warehouse-resources/locations/pages/LocationsView.vue index d1ed0bfa..c3c1e92c 100644 --- a/src/modules/warehouse-resources/locations/pages/LocationsView.vue +++ b/src/modules/warehouse-resources/locations/pages/LocationsView.vue @@ -2,7 +2,6 @@ import { computed, onMounted, reactive, ref, watch } from 'vue' import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus' import { Eye, MapPin, MoreHorizontal, Pencil, Plus, Power, Trash2 } from '@lucide/vue' -import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam' import { useAccessPreviewStore } from '@/core/access/accessPreview' import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration' import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http' @@ -43,9 +42,6 @@ type AppliedLocationFilters = { } const access = useAccessPreviewStore() -const customers = ref([]) -const customerLoading = ref(false) -const customerError = ref(null) const warehouses = ref([]) const warehouseLoading = ref(false) const warehouseError = ref(null) @@ -82,7 +78,7 @@ const queryFields = computed(() => [ }, options: warehouses.value.map((warehouse) => ({ value: String(warehouse.id), - label: `${warehouse.warehouseName}(${warehouse.warehouseCode}) · ${customerLabel(warehouse.tenantId)}`, + label: `${warehouse.warehouseName}(${warehouse.warehouseCode})`, })), }, { key: 'zoneCode', type: 'input', width: 210, placeholder: '库区编码', props: { maxlength: 50, 'aria-label': '库区编码' } }, @@ -232,32 +228,13 @@ function recordScope(row: PlatformLocationResource) { return { customerId: apiId(String(row.tenantId)), warehouseId: apiId(String(row.warehouseId)) } } -function customerLabel(customerId: number | string) { - const customer = customers.value.find((item) => String(item.id) === String(customerId)) - return customer ? `${customer.tenantName}(${customer.tenantCode || customer.id})` : String(customerId) -} - -async function loadCustomers() { - customerLoading.value = true - customerError.value = null - try { - const result = await listPlatformTenants({ page: 1, size: 200 }) - customers.value = result.records - } catch (error) { - customers.value = [] - customerError.value = getApiErrorDetails(error, '所属企业列表加载失败,请稍后重试') - } finally { - customerLoading.value = false - } -} - async function loadWarehouses() { const sequence = ++warehouseLoadSequence warehouseLoading.value = true warehouseError.value = null warehouses.value = [] try { - const result = await listPlatformWarehouses(undefined, 1, 200) + const result = await listPlatformWarehouses(1, 200) if (sequence !== warehouseLoadSequence) return warehouses.value = result.records } catch (error) { @@ -482,7 +459,6 @@ watch(() => form.warehouseId, (warehouseId) => { }) onMounted(() => { - void loadCustomers() void loadWarehouses() void loadLocations() }) @@ -497,12 +473,6 @@ onMounted(() => { @search="applyFilters" /> - - -

{{ customerError.message }}

- - 重新加载所属企业 -

{{ warehouseError.message }}

@@ -637,7 +607,7 @@ onMounted(() => { {{ statusMeta[detail.location.status].label }} - {{ customerLabel(detail.location.tenantId) }} + 平台公共资源 {{ detail.warehouseName }}({{ detail.warehouseCode }}) {{ detail.location.locationCode }} {{ detail.location.locationName || '—' }} @@ -677,21 +647,21 @@ onMounted(() => { >
-
{{ detail?.warehouseName || selectedWarehouse?.warehouseName }}({{ detail?.warehouseCode || selectedWarehouse?.warehouseCode }})所属企业:{{ customerLabel(form.customerId) }} · 保存后不可跨仓移动
+
{{ detail?.warehouseName || selectedWarehouse?.warehouseName }}({{ detail?.warehouseCode || selectedWarehouse?.warehouseCode }})平台公共资源 · 保存后不可跨仓移动

库位标识

-

库位编码在当前企业和仓库内唯一;编辑后不能移动到其他仓库。

+

库位编码在当前仓库内唯一;编辑后不能移动到其他仓库。

diff --git a/src/modules/warehouse-resources/pallets/pages/PalletsView.test.ts b/src/modules/warehouse-resources/pallets/pages/PalletsView.test.ts index d11a4c39..65e399ab 100644 --- a/src/modules/warehouse-resources/pallets/pages/PalletsView.test.ts +++ b/src/modules/warehouse-resources/pallets/pages/PalletsView.test.ts @@ -4,7 +4,6 @@ import { enableAutoUnmount, flushPromises, mount, type VueWrapper } from '@vue/t import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { useAccessPreviewStore } from '@/core/access/accessPreview' import { useAuthStore } from '@/core/auth/auth' -import { listPlatformTenants as listPlatformCustomers, type PlatformTenantResource as PlatformCustomerResource } from '@/modules/iam' import { listPlatformWarehouses, type PlatformWarehouseResource } from '@/modules/warehouses' import DataTable from '@/shared/components/DataTable.vue' import QueryToolbar from '@/shared/components/QueryToolbar.vue' @@ -16,11 +15,6 @@ import { } from '../api/platformPallets' import PalletsView from './PalletsView.vue' -vi.mock('@/modules/iam', async (importOriginal) => { - const actual = await importOriginal() - const listPlatformTenants = vi.fn() - return { ...actual, listPlatformTenants } -}) vi.mock('@/modules/warehouses', () => ({ listPlatformWarehouses: vi.fn() })) vi.mock('../api/platformPallets', () => ({ listPlatformPallets: vi.fn(), @@ -48,23 +42,14 @@ function createMemoryStorage(): Storage { } } -const customer = (id: number | string, tenantName: string): PlatformCustomerResource => ({ - id, tenantCode: String(id), tenantName, status: 'ACTIVE', legalName: tenantName, - legalRepresentative: null, unifiedSocialCreditCode: null, contactName: null, - contactPhone: null, contactEmail: null, servicePackage: null, contactWechat: null, - salesOwnerName: null, paymentTermDays: null, enterpriseType: null, establishedDate: null, - registeredCapital: null, registeredCapitalCurrency: null, registeredAddress: null, - internalRemark: null, createdAt: null, approvedAt: null, -}) - const warehouse: PlatformWarehouseResource = { - id: '19001', tenantId: 10001, warehouseCode: 'CN-YW-01', warehouseName: '义乌收货仓', + id: '19001', tenantId: 0, warehouseCode: 'CN-YW-01', warehouseName: '义乌收货仓', warehouseType: 'DOMESTIC', countryCode: 'CN', timezone: 'Asia/Shanghai', contactName: null, contactPhone: null, contactEmail: null, status: 'ACTIVE', remark: null, } const pallet: PlatformPalletResource = { - id: '20001', tenantId: 10001, warehouseId: '19001', palletCode: 'TP-001', palletType: '木托盘', + id: '20001', tenantId: 0, warehouseId: '19001', palletCode: 'TP-001', palletType: '木托盘', capacityCbm: '1.200000', remark: '标准川字托盘', createdAt: '2026-08-04T09:00:00', updatedAt: '2026-08-04T10:00:00', } @@ -110,9 +95,6 @@ describe('PalletsView', () => { beforeEach(() => { vi.stubGlobal('ResizeObserver', ResizeObserverStub) document.body.innerHTML = '' - vi.mocked(listPlatformCustomers).mockReset().mockResolvedValue({ - records: [customer(10001, '默认企业'), customer('9223372036854775806', '雪花企业')], page: 1, size: 200, total: 2, pages: 1, - }) vi.mocked(listPlatformWarehouses).mockReset().mockResolvedValue({ records: [warehouse], page: 1, size: 200, total: 1, pages: 1 }) vi.mocked(listPlatformPallets).mockReset().mockResolvedValue({ records: [pallet], page: 1, size: 20, total: 1, pages: 1 }) vi.mocked(getPlatformPallet).mockReset().mockResolvedValue(pallet) @@ -132,7 +114,7 @@ describe('PalletsView', () => { ]) expect(wrapper.find('.pallet-filter-toolbar').exists()).toBe(true) expect(fields.find((field) => field.key === 'warehouseId')?.placeholder).toBe('全部仓库') - expect(listPlatformWarehouses).toHaveBeenCalledWith(undefined, 1, 200) + expect(listPlatformWarehouses).toHaveBeenCalledWith(1, 200) expect(listPlatformPallets).toHaveBeenCalledWith({ page: 1, size: 20, }) @@ -143,7 +125,7 @@ describe('PalletsView', () => { await loadRows(wrapper) expect(listPlatformWarehouses).toHaveBeenCalledTimes(1) - expect(listPlatformWarehouses).toHaveBeenCalledWith(undefined, 1, 200) + expect(listPlatformWarehouses).toHaveBeenCalledWith(1, 200) expect(listPlatformPallets).toHaveBeenCalledWith({ page: 1, size: 20, }) @@ -234,7 +216,7 @@ describe('PalletsView', () => { await saveButton?.trigger('click') await flushPromises() - expect(createPlatformPallet).toHaveBeenCalledWith(10001, { + expect(createPlatformPallet).toHaveBeenCalledWith(0, { warehouseId: 19001, palletCode: 'TP-002', palletType: '塑料托盘', capacityCbm: 1.5, remark: '蓝色标准托盘', }) }) @@ -277,33 +259,33 @@ describe('PalletsView', () => { expect(wrapper.text()).not.toContain('TP-001') }) - it('keeps platform warehouse and pallet queries available when customer names cannot load', async () => { - vi.mocked(listPlatformCustomers).mockRejectedValueOnce(new Error('Gateway unavailable')) + it('keeps platform warehouse and pallet queries independent from customer names', async () => { const wrapper = await mountView() - expect(wrapper.text()).toContain('客户列表暂时无法加载') - expect(wrapper.text()).toContain('重新加载') - expect(listPlatformWarehouses).toHaveBeenCalledWith(undefined, 1, 200) + expect(wrapper.text()).not.toContain('客户列表暂时无法加载') + expect(wrapper.text()).not.toContain('所属企业') + expect(listPlatformWarehouses).toHaveBeenCalledWith(1, 200) expect(listPlatformPallets).toHaveBeenCalledWith({ page: 1, size: 20, }) }) it('shows only the pallet fields exposed by the backend contract', async () => { - const crossCustomerPallet = { ...pallet, tenantId: '9223372036854775806' } - vi.mocked(listPlatformPallets).mockResolvedValue({ records: [crossCustomerPallet], page: 1, size: 20, total: 1, pages: 1 }) - vi.mocked(getPlatformPallet).mockResolvedValue(crossCustomerPallet) + const publicPallet = { ...pallet, tenantId: '0' } + vi.mocked(listPlatformPallets).mockResolvedValue({ records: [publicPallet], page: 1, size: 20, total: 1, pages: 1 }) + vi.mocked(getPlatformPallet).mockResolvedValue(publicPallet) const wrapper = await mountView() await loadRows(wrapper) - expect(wrapper.findComponent(DataTable).props('data')).toEqual([crossCustomerPallet]) + expect(wrapper.findComponent(DataTable).props('data')).toEqual([publicPallet]) const tableBody = wrapper.find('.el-table__body-wrapper') const viewButton = tableBody.findAll('.pallet-row-actions button').find((button) => button.text().trim() === '查看') await viewButton?.trigger('click') await flushPromises() - expect(getPlatformPallet).toHaveBeenCalledWith('9223372036854775806', 19001, '20001') - expect(document.body.textContent).toContain('所属企业') - expect(document.body.textContent).toContain('雪花企业(9223372036854775806)') + expect(getPlatformPallet).toHaveBeenCalledWith(0, 19001, '20001') + expect(document.body.textContent).toContain('资源作用域') + expect(document.body.textContent).toContain('平台公共资源') + expect(document.body.textContent).not.toContain('所属企业') expect(document.body.textContent).toContain('托盘编码') expect(document.body.textContent).toContain('托盘类型') expect(document.body.textContent).toContain('托盘容积') diff --git a/src/modules/warehouse-resources/pallets/pages/PalletsView.vue b/src/modules/warehouse-resources/pallets/pages/PalletsView.vue index af9eee7b..48ee3bce 100644 --- a/src/modules/warehouse-resources/pallets/pages/PalletsView.vue +++ b/src/modules/warehouse-resources/pallets/pages/PalletsView.vue @@ -4,7 +4,6 @@ import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'elem import { Boxes, Eye, Pencil, Plus, Trash2, Warehouse as WarehouseIcon } from '@lucide/vue' import { useAccessPreviewStore } from '@/core/access/accessPreview' import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration' -import { listPlatformTenants as listPlatformCustomers, type PlatformTenantResource as PlatformCustomerResource } from '@/modules/iam' import { listPlatformWarehouses, type PlatformWarehouseResource } from '@/modules/warehouses' import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http' import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue' @@ -21,9 +20,6 @@ import { } from '../api/platformPallets' const access = useAccessPreviewStore() -const customers = ref([]) -const customerLoading = ref(false) -const customerError = ref(null) const warehouses = ref([]) const warehouseLoading = ref(false) const warehouseError = ref(null) @@ -140,44 +136,17 @@ function warehouseFor(warehouseId: number | string) { return warehouses.value.find((item) => String(item.id) === String(warehouseId)) || null } -function customerLabel(customerId: number | string) { - const customer = customers.value.find((item) => String(item.id) === String(customerId)) - return customer ? `${customer.tenantName}(${customer.tenantCode || customer.id})` : '—' -} - function errorMessage(details: ApiErrorDetails) { return details.requestId ? `${details.message}(requestId: ${details.requestId})` : details.message } -async function loadCustomers() { - customerLoading.value = true - customerError.value = null - try { - const records: PlatformCustomerResource[] = [] - let currentPage = 1 - let pages = 1 - do { - const result = await listPlatformCustomers({ page: currentPage, size: 200 }) - records.push(...result.records) - pages = Math.max(1, result.pages) - currentPage += 1 - } while (currentPage <= pages) - customers.value = records - } catch (error) { - customers.value = [] - customerError.value = getApiErrorDetails(error, '所属企业列表加载失败,请稍后重试') - } finally { - customerLoading.value = false - } -} - async function loadWarehouses() { const sequence = ++warehouseSequence warehouses.value = [] warehouseError.value = null warehouseLoading.value = true try { - const result = await listPlatformWarehouses(undefined, 1, 200) + const result = await listPlatformWarehouses(1, 200) if (sequence !== warehouseSequence) return warehouses.value = result.records } catch (error) { @@ -343,7 +312,6 @@ watch(() => access.can(editorPermission.value), (canWrite) => { if (!canWrite && editorOpen.value) editorOpen.value = false }, { flush: 'sync' }) onMounted(() => { - void loadCustomers() void loadWarehouses() void loadPallets() }) @@ -351,14 +319,6 @@ onMounted(() => {