diff --git a/src/modules/self-operated/components/InventoryTurnoverView.test.ts b/src/modules/self-operated/components/InventoryTurnoverView.test.ts index 320eb90f..50a46570 100644 --- a/src/modules/self-operated/components/InventoryTurnoverView.test.ts +++ b/src/modules/self-operated/components/InventoryTurnoverView.test.ts @@ -13,7 +13,7 @@ vi.mock('../api/analytics', () => ({ analyticsApi: mocks })) vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => mocks.allowed }) })) const toolbar = defineComponent({ name: 'QueryToolbar', props: ['modelValue', 'fields'], emits: ['update:modelValue', 'search', 'reset'], setup(_, { slots }) { return () => h('div', slots.actions?.()) } }) const table = defineComponent({ name: 'DataTable', props: ['data', 'columns', 'total', 'page'], emits: ['update:page', 'update:pageSize'], setup(p, { slots }) { return () => h('div', [slots.filters?.(), slots['toolbar-start']?.(), slots['toolbar-actions']?.(), slots['before-results']?.(), ...(p.columns ?? []).map((column: { headerSlotName?: string; prop: string; label: string }) => column.headerSlotName ? slots[column.headerSlotName]?.({ column: { property: column.prop, label: column.label } }) : null), ...(p.data ?? []).flatMap((row: TurnoverRow) => (p.columns ?? []).map((column: { slotName?: string; prop: keyof TurnoverRow }) => column.slotName && slots[column.slotName] ? slots[column.slotName]?.({ row }) : String(row[column.prop] ?? '')))]) } }) -const button = defineComponent({ emits: ['click'], setup(_, { slots, emit }) { return () => h('button', { onClick: () => emit('click') }, slots.default?.()) } }) +const button = defineComponent({ emits: ['click'], setup(_, { slots, emit }) { return () => h('button', { onClick: (event: MouseEvent) => emit('click', event) }, slots.default?.()) } }) const shell = defineComponent({ setup(_, { slots }) { return () => h('div', slots.default?.()) } }) const total = { value: 100, knownSubtotal: 100, unknownSkuCount: 0, reasonCodes: [] } const risk = { matchedSkuCount: 0, knownPhysicalQuantity: 0, unknownSkuCount: 0, physicalQuantityRatio: 0 } @@ -208,6 +208,26 @@ describe('inventory turnover page fixed snapshot behavior', () => { await w.get('button[data-column="averageDailyOutboundQuantity"]').trigger('click'); await flushPromises() expect(mocks.turnover.mock.calls.at(-1)![0]).toMatchObject({ baseWindowDays: 7, sortBy: 'AVERAGE_DAILY_DESC' }) }) + it.each(['SKU', 'OWNER'] as const)('sorts the default percentage column for %s and keeps formula help independent', async (groupBy) => { + const w = page(); await flushPromises() + await change(w, { groupBy, customerCodes: ['OWNER-A'], rangePreset: 'LAST_7_DAYS' }, true) + w.findComponent(table).vm.$emit('update:page', 2); await flushPromises() + expect(w.find('button[data-column="quantityTurnoverCount"]').exists()).toBe(false) + const before = mocks.turnover.mock.calls.length + await w.get('button[aria-label="期间周转率计算说明"]').trigger('click'); await flushPromises() + expect(mocks.turnover).toHaveBeenCalledTimes(before) + expect(w.text()).toContain('期间周转率=所选 7 天完整出库量 ÷ 同期平均实物库存 × 100%') + expect(w.text()).toContain('平均实物库存=同期每日末实物库存之和 ÷ 天数') + const header = w.get('button[data-column="quantityTurnoverRate"]') + for (const direction of ['DESC', 'ASC']) { + await header.trigger('click'); await flushPromises() + expect(mocks.turnover.mock.calls.at(-1)![0]).toMatchObject({ groupBy, customerCodes: ['OWNER-A'], baseWindowDays: 7, endDate: '2026-10-12', hideZeroInventory: true, sortBy: `TURNOVER_${direction}`, current: 1, snapshotContext: 'token-A', includeOverview: false }) + expect(header.attributes('aria-label')).toContain(direction === 'ASC' ? '当前升序' : '当前降序') + expect(w.findComponent(toolbar).props('modelValue').sortBy).toBe(`TURNOVER_${direction}`) + } + await w.findAll('button').find(b => b.text() === '扩展列')!.trigger('click') + expect(w.get('button[data-column="quantityTurnoverCount"]').attributes('aria-label')).toContain('当前升序') + }) it('keeps the successful sort marker and page when a header query fails', async () => { const w = page(); await flushPromises() w.findComponent(table).vm.$emit('update:page', 2); await flushPromises() diff --git a/src/modules/self-operated/components/InventoryTurnoverView.vue b/src/modules/self-operated/components/InventoryTurnoverView.vue index 5131bf6d..0255212f 100644 --- a/src/modules/self-operated/components/InventoryTurnoverView.vue +++ b/src/modules/self-operated/components/InventoryTurnoverView.vue @@ -91,7 +91,7 @@ const columns = computed(() => [ { prop: 'transitQuantity', label: '在途', slotName: 'transitQuantity', minWidth: 125 }, ...([7, 15, 30] as const).map(n => ({ prop: `outbound${n}`, label: `${n}日出库`, slotName: `outbound${n}`, minWidth: 160 })), { prop: 'averageDailyOutboundQuantity', label: `${applied.value?.baseWindowDays ?? 30}日日均`, slotName: 'daily', minWidth: 110 }, - { prop: 'quantityTurnoverRate', label: '期间周转率', slotName: 'turnoverRate', minWidth: 145 }, + { prop: 'quantityTurnoverRate', label: '期间周转率', slotName: 'turnoverRate', minWidth: 180 }, ...(ownerMode.value ? [{ prop: 'noOutboundInWindow', label: '无出库SKU / 实物', slotName: 'noOutbound', minWidth: 190 }, { prop: 'slowCoverage', label: '慢消化SKU / 待判断', slotName: 'slow', minWidth: 175 }] : [{ prop: 'inventoryCoverageDays', label: '预计可售天数', slotName: 'coverage', minWidth: 150 }]), ...(expanded.value ? [{ prop: 'frozenQuantity', label: '冻结件数', slotName: 'frozenQuantity', minWidth: 125 }, { prop: 'defectiveQuantity', label: '次品实物', slotName: 'defectiveQuantity', minWidth: 125 }, { prop: 'averagePhysicalQuantity', label: '期间平均实物', slotName: 'averageStock', minWidth: 145 }, { prop: 'quantityTurnoverCount', label: '期间件数周转', slotName: 'turnover', minWidth: 145 }, { prop: 'lastObservedOutboundDate', label: '最近已观测出库日', slotName: 'lastDate', minWidth: 180 }] : []), ].map(column => ({ ...column, headerSlotName: turnoverSortColumns[column.prop] ? 'sortHeader' : undefined }))) @@ -279,10 +279,22 @@ onUnmounted(() => { exportController?.abort(); fence.cancel(); runtimeFence.canc

{{ inputError }}

{{ notice }}