- | 日期(点击查看) | 正式量 | 已采集量 | 状态 | 原因 |
|---|
| {{ quantityText(point.outboundQuantity) }} | {{ quantityText(point.observedQuantity) }} | {{ label(point.dataStatus) }} | {{ reasonsText(point.reasonCodes) }} |
-
-
+
| 日期(点击查看) | 正式量 | 已采集量 | 状态 | 原因 |
|---|
| {{ quantityText(point.outboundQuantity) }} | {{ quantityText(point.observedQuantity) }} | {{ label(point.dataStatus) }} | {{ reasonsText(point.reasonCodes) }} |
+
+
实线/实心点:完整日 · 虚线/空心点:未完整日 · 未知及缺失日期断线;今日为已采集累计
+
+
所选日期暂无可靠出库资料,未知不按零显示。
diff --git a/src/modules/self-operated/components/InventoryTurnoverView.test.ts b/src/modules/self-operated/components/InventoryTurnoverView.test.ts
index 68b247f0..1c8bc754 100644
--- a/src/modules/self-operated/components/InventoryTurnoverView.test.ts
+++ b/src/modules/self-operated/components/InventoryTurnoverView.test.ts
@@ -17,7 +17,7 @@ const volume = { value: 1.25, knownSubtotal: 1.25, missingDimensionQuantity: 0,
const volumes = [7, 15, 30].map(days => ({ days, startDate: '2026-09-13', endDateExclusive: '2026-10-13', volume }))
const row: TurnoverRow = { inventoryVolumeM3: volume, outboundVolumesM3: volumes, unitVolumeM3: 0.0125, dimensionSource: 'WAREHOUSE_MEASURED', customerCode: 'OWNER-A', customerName: null, sku: 'SAME', productName: '商品', physicalQuantity: total, availableQuantity: total, frozenQuantity: total, defectiveQuantity: total, transitQuantity: total, physicalSkuCount: 1, windows: [7, 15, 30].map(days => ({ days: days as 7 | 15 | 30, startDate: '2026-09-13', endDateExclusive: '2026-10-13', outboundQuantity: 300, observedQuantity: 300, averageDailyOutboundQuantity: 10, completeDayCount: days, partialDayCount: 0, unknownDayCount: 0, dataStatus: 'COMPLETE', reasonCodes: [] })), averageDailyOutboundQuantity: 10, inventoryCoverageDays: 10, coverageStatus: 'VALID', averagePhysicalQuantity: null, quantityTurnoverCount: null, turnoverStatus: 'INSUFFICIENT', qualifiedSampleDays: 0, requiredSampleDays: 30, missingSampleDates: ['2026-10-12'], sampleReasonCodes: [], lastObservedOutboundDate: null, attentionReasons: [], noOutboundInWindow: risk, slowCoverage: risk, unassessableSkuCount: 0 }
function response(q: TurnoverQuery): TurnoverResponse {
- return { context: { snapshotContext: q.snapshotContext || `token-${q.warehouseCode}`, sourceAccount: q.sourceAccount, warehouseCode: q.warehouseCode, endDate: q.endDate, baseWindowDays: q.baseWindowDays, queryAsOf: '2026-10-13T00:00:00Z', queryExpiresAt: '2099-10-13T00:30:00Z', inventoryObservedFrom: null, inventoryObservedUntil: null, sampleRuleVersion: 'DAILY_SCHEDULED_FIRST_V1', dailySamples: [], missingSampleDates: ['2026-10-12'], accumulationStartedOn: null, analyticsContext: { salesRunId: '11', inventoryRunId: '22', salesScopeHash: 'a', inventoryScopeHash: 'b', queryAsOf: '2026-10-13T00:00:00Z', queryExpiresAt: '2099-10-13T00:30:00Z', businessTimezone: 'Asia/Shanghai', anchorDate: '2026-10-13' } }, summary: q.includeOverview ? { inventoryVolumeM3: volume, outboundVolumesM3: volumes, physicalQuantity: total, availableQuantity: total, noOutboundSkuCount: total, unassessableSkuCount: total } : null, charts: q.includeOverview ? { ownerPhysicalTop: [], unknownOwnerCount: 0, averageDaily: row.windows } : null, rows: [{ ...row, sku: q.groupBy === 'OWNER' ? null : row.sku }], total: 40 }
+ return { context: { snapshotContext: q.snapshotContext || `token-${q.warehouseCode}`, sourceAccount: q.sourceAccount, warehouseCode: q.warehouseCode, endDate: q.endDate, baseWindowDays: q.baseWindowDays, queryAsOf: '2026-10-13T00:00:00Z', queryExpiresAt: '2099-10-13T00:30:00Z', inventoryObservedFrom: null, inventoryObservedUntil: null, sampleRuleVersion: 'DAILY_SCHEDULED_FIRST_V1', dailySamples: [], missingSampleDates: ['2026-10-12'], accumulationStartedOn: null, analyticsContext: { salesRunId: '11', inventoryRunId: '22', salesScopeHash: 'a', inventoryScopeHash: 'b', queryAsOf: '2026-10-13T00:00:00Z', queryExpiresAt: '2099-10-13T00:30:00Z', businessTimezone: 'Asia/Shanghai', anchorDate: '2026-10-13' } }, summary: q.includeOverview ? { inventoryVolumeM3: volume, outboundVolumesM3: volumes, physicalQuantity: total, availableQuantity: total, noOutboundSkuCount: total, unassessableSkuCount: total } : null, charts: q.includeOverview ? { ownerPhysicalTop: [], unknownOwnerCount: 0, ownerVolumeTop: [], unknownVolumeOwnerCount: 0, averageDaily: row.windows } : null, rows: [{ ...row, sku: q.groupBy === 'OWNER' ? null : row.sku }], total: 40 }
}
const wrappers: ReturnType
[] = []
function page() { const w = mount(InventoryTurnoverView, { global: { stubs: { AdminPage: shell, DataTable: table, QueryToolbar: toolbar, AnalyticsChart: true, ChartDetailsDialog: true, AppDialog: true, ApiErrorMeta: true, 'el-button': button, 'el-tag': shell, 'el-popover': defineComponent({ setup(_, { slots }) { return () => h('div', [slots.reference?.(), slots.default?.()]) } }) } } }); wrappers.push(w); return w }
@@ -28,6 +28,32 @@ async function change(w: ReturnType, patch: object, search = false
beforeEach(() => { vi.clearAllMocks(); mocks.ownerDirectory.mockResolvedValue([{ customerCode: "OWNER-A", customerName: "货主甲" }]); mocks.allowed = true; mocks.route = reactive({ query: { endDate: '2026-10-12' } }); mocks.overview.mockResolvedValue({ warehouses: [{ warehouseCode: 'A' }, { warehouseCode: 'B' }] }); mocks.turnover.mockImplementation(async (q: TurnoverQuery) => response(q)); sessionStorage.clear() })
afterEach(() => wrappers.splice(0).forEach(w => w.unmount()))
describe('inventory turnover page fixed snapshot behavior', () => {
+ it('defaults to volume ranking, switches locally and drills into the same owner snapshot', async () => {
+ mocks.turnover.mockImplementation(async (q: TurnoverQuery) => {
+ const result = response(q)
+ if (result.charts) {
+ result.charts.ownerVolumeTop = [{ customerCode: 'OWNER-A', customerName: '货主甲', inventoryVolumeM3: { ...volume, value: 1234.56789 } }]
+ result.charts.unknownVolumeOwnerCount = 2
+ }
+ return result
+ })
+ const w = page(); await flushPromises()
+ expect(w.get('[aria-label="图表指标"] button[aria-pressed="true"]').text()).toBe('货主库存立方数 Top10')
+ expect(w.text()).toContain('2 个货主因缺尺寸或库存数量未知,未参与排名')
+ const chart = w.findComponent({ name: 'AnalyticsChart' })
+ const option = chart.props('option') as { series: { data: number[]; label: { position: string; formatter: (p: {value: number}) => string } }[] }
+ expect(option.series[0]!.data).toEqual([1234.56789])
+ expect(option.series[0]!.label.position).toBe('right')
+ expect(option.series[0]!.label.formatter({ value: 1234.56789 })).toBe('1,234.57 m³')
+ const calls = mocks.turnover.mock.calls.length
+ await w.get('[aria-label="图表指标"] button:last-child').trigger('click')
+ expect(w.get('[aria-label="图表指标"] button[aria-pressed="true"]').text()).toBe('日均出库对比')
+ await w.get('[aria-label="图表指标"] button:first-child').trigger('click')
+ expect(mocks.turnover).toHaveBeenCalledTimes(calls)
+ w.findComponent({ name: 'AnalyticsChart' }).vm.$emit('select', { dataIndex: 0 }); await flushPromises()
+ expect(mocks.turnover.mock.calls.at(-1)![0]).toMatchObject({ customerCode: 'OWNER-A', groupBy: 'SKU', current: 1, snapshotContext: 'token-A' })
+ })
+
it('shows customer and SKU volume, missing dimensions and estimates in the selected window', async () => {
mocks.turnover.mockImplementation(async (q: TurnoverQuery) => {
const result = response(q)
@@ -58,7 +84,7 @@ describe('inventory turnover page fixed snapshot behavior', () => {
it('sorts every numeric header on the server in both directions with the same snapshot', async () => {
const w = page(); await flushPromises()
await w.findAll('button').find(b => b.text() === '扩展列')!.trigger('click')
- const metrics = { physicalQuantity: 'PHYSICAL', availableQuantity: 'AVAILABLE', transitQuantity: 'TRANSIT', outbound7: 'OUTBOUND_7', outbound15: 'OUTBOUND_15', outbound30: 'OUTBOUND_30', averageDailyOutboundQuantity: 'AVERAGE_DAILY', inventoryCoverageDays: 'COVERAGE', frozenQuantity: 'FROZEN', defectiveQuantity: 'DEFECTIVE', averagePhysicalQuantity: 'AVERAGE_PHYSICAL', quantityTurnoverCount: 'TURNOVER' }
+ const metrics = { inventoryVolumeM3: 'INVENTORY_VOLUME', outboundVolumesM3: 'OUTBOUND_VOLUME', unitVolumeM3: 'UNIT_VOLUME', physicalQuantity: 'PHYSICAL', availableQuantity: 'AVAILABLE', transitQuantity: 'TRANSIT', outbound7: 'OUTBOUND_7', outbound15: 'OUTBOUND_15', outbound30: 'OUTBOUND_30', averageDailyOutboundQuantity: 'AVERAGE_DAILY', inventoryCoverageDays: 'COVERAGE', frozenQuantity: 'FROZEN', defectiveQuantity: 'DEFECTIVE', averagePhysicalQuantity: 'AVERAGE_PHYSICAL', quantityTurnoverCount: 'TURNOVER' }
for (const [prop, metric] of Object.entries(metrics)) {
// Start on a later page and with another sort to prove that a header resets pagination.
await change(w, { sortBy: metric === 'PHYSICAL' ? 'AVAILABLE_DESC' : 'PHYSICAL_DESC' }, true)
@@ -90,6 +116,11 @@ describe('inventory turnover page fixed snapshot behavior', () => {
const w = page(); await flushPromises()
await change(w, { groupBy: 'OWNER' })
expect(w.find('button[data-column="inventoryCoverageDays"]').exists()).toBe(false)
+ expect(w.find('button[data-column="unitVolumeM3"]').exists()).toBe(false)
+ for (const [prop, metric] of [['inventoryVolumeM3', 'INVENTORY_VOLUME'], ['outboundVolumesM3', 'OUTBOUND_VOLUME']]) {
+ await w.get(`button[data-column="${prop}"]`).trigger('click'); await flushPromises()
+ expect(mocks.turnover.mock.calls.at(-1)![0]).toMatchObject({ groupBy: 'OWNER', sortBy: `${metric}_DESC` })
+ }
await w.get('button[data-column="averageDailyOutboundQuantity"]').trigger('click'); await flushPromises()
expect(mocks.turnover.mock.calls.at(-1)![0]).toMatchObject({ groupBy: 'OWNER', sortBy: 'AVERAGE_DAILY_DESC' })
w.findComponent(toolbar).vm.$emit('reset'); await flushPromises()
@@ -109,6 +140,7 @@ describe('inventory turnover page fixed snapshot behavior', () => {
return result
})
const w = page(); await flushPromises()
+ await w.get('[aria-label="图表指标"] button:last-child').trigger('click')
const chart = w.findComponent({ name: 'AnalyticsChart' }).props('option') as { series: { data: (number | null)[]; label: { show: boolean; formatter: (params: { value: number | null }) => string } }[] }
expect(chart.series[0]!.label.show).toBe(true)
expect(chart.series[0]!.label.formatter({ value: chart.series[0]!.data[0]! })).toBe('1,234.57')
@@ -165,14 +197,14 @@ describe('inventory turnover page fixed snapshot behavior', () => {
expect(w.text()).toContain('当前库存已过期')
expect(w.text()).toContain('库存分页出现重复记录')
expect(w.text()).toContain('LXCOL-TEST-STOCK')
- expect(w.findAll('.turnover-cards:not(.turnover-volume-cards) article strong').map(n => n.text())).toEqual(['—', '100', '—', '100'])
- expect(w.findAll('.turnover-cards article small')[2]!.text()).toBe('已知小计 0 · 5402 SKU待核验')
+ expect(w.findAll('.turnover-cards article[data-kind="quantity"] strong').map(n => n.text())).toEqual(['—', '100', '—', '100'])
+ expect(w.get('article[data-metric="no-outbound"] small').text()).toBe('已知小计 0 · 5402 SKU待核验')
mocks.overview.mockResolvedValue({ warehouses: [{ warehouseCode: 'A', dataFamily: 'INVENTORY', currentTask: { taskNo: 'LXCOL-TEST-STOCK', taskStatus: 'SUCCEEDED' } }] })
mocks.turnover.mockImplementation(async (q: TurnoverQuery) => { const result = response(q); result.context.analyticsContext.inventoryStatus = 'VALID'; return result })
await w.findAll('button').find(b => b.text() === '整体刷新')!.trigger('click'); await flushPromises()
expect(mocks.overview).toHaveBeenCalledTimes(2)
expect(w.text()).not.toContain('当前库存已过期'); expect(w.text()).not.toContain('库存分页出现重复记录')
- expect(w.findAll('.turnover-cards:not(.turnover-volume-cards) article strong')[0]!.text()).toBe('100')
+ expect(w.findAll('.turnover-cards article[data-kind="quantity"] strong')[0]!.text()).toBe('100')
})
it('does not show another warehouse failure and separates runtime refresh failure from report data', async () => {
mocks.overview.mockResolvedValue({ warehouses: [{ warehouseCode: 'A', dataFamily: 'INVENTORY' }, { warehouseCode: 'B', dataFamily: 'INVENTORY', currentTask: { taskNo: 'LXCOL-OTHER', taskStatus: 'DEAD', latestAttempt: { status: 'FAILED', errorCode: 'SELFOP_STOCK_DUPLICATE' } } }] })
@@ -182,7 +214,7 @@ describe('inventory turnover page fixed snapshot behavior', () => {
await w.findAll('button').find(b => b.text() === '整体刷新')!.trigger('click'); await flushPromises()
expect(w.text()).toContain('采集状态更新失败')
expect(w.text()).not.toContain('库存周转读取失败')
- expect(w.findAll('.turnover-cards:not(.turnover-volume-cards) article strong')[0]!.text()).toBe('100')
+ expect(w.findAll('.turnover-cards article[data-kind="quantity"] strong')[0]!.text()).toBe('100')
})
it('renders quantity and window cells through the shared table slot contract', async () => {
mocks.turnover.mockImplementation(async (q: TurnoverQuery) => {
@@ -196,7 +228,7 @@ describe('inventory turnover page fixed snapshot behavior', () => {
expect(w.text()).toContain('数据不足/待观察')
expect(w.text()).toContain('300')
expect(w.text()).toContain('暂无可排名的库存数据')
- expect(w.findAll('.turnover-cards:not(.turnover-volume-cards) article strong').map(n => n.text())).toEqual(['100', '100', '100', '100'])
+ expect(w.findAll('.turnover-cards article[data-kind="quantity"] strong').map(n => n.text())).toEqual(['100', '100', '100', '100'])
expect(w.findAllComponents({ name: 'AppDialog' }).every(d => d.attributes('append-to-body') !== undefined)).toBe(true)
})
it('honors an explicit new route token in the same scope and reloads its overview', async () => {
diff --git a/src/modules/self-operated/components/InventoryTurnoverView.vue b/src/modules/self-operated/components/InventoryTurnoverView.vue
index d2e3364a..dcc7091a 100644
--- a/src/modules/self-operated/components/InventoryTurnoverView.vue
+++ b/src/modules/self-operated/components/InventoryTurnoverView.vue
@@ -16,10 +16,10 @@ import { RequestFence, beijingTime, contextParams } from '../state'
import type { DailyResponse, Overview } from '../types'
import { overviewRange, overviewRangeOptions, beijingDate } from '../overviewRange'
import type { RangePreset } from '../types'
-import type { TurnoverQuery, TurnoverResponse, TurnoverRow, TurnoverSort, TurnoverWindow } from '../turnoverTypes'
+import type { TurnoverQuery, TurnoverResponse, TurnoverRow, TurnoverSort, TurnoverWindow, TurnoverTotal, TurnoverVolume } from '../turnoverTypes'
import { turnoverRoute, validateTurnover, yesterdayBeijing } from '../turnoverNavigation'
import { turnoverSortColumns, turnoverSortOptions } from '../turnoverSorting'
-import { totalText, turnoverTotalNote, volumeNote, dimensionSourceText, inventoryCollectionNotice, turnoverChart, turnoverNumber, windowText, turnoverReasonsText as reasonsText } from '../turnoverPresentation'
+import { totalText, turnoverTotalNote, volumeText, volumeNote, dimensionSourceText, inventoryCollectionNotice, turnoverChart, turnoverNumber, windowText, turnoverReasonsText as reasonsText } from '../turnoverPresentation'
import AnalyticsChart from './AnalyticsChart.vue'
import ChartDetailsDialog from './ChartDetailsDialog.vue'
import SkuTrend from './SkuTrend.vue'
@@ -34,6 +34,7 @@ const runtime = ref(null), data = ref(
const error = ref(null), inputError = ref(''), notice = ref(''), loading = ref(false)
const filters = ref({ warehouseCode: '', endDate: yesterdayBeijing(), baseWindowDays: 30, rangePreset: 'LAST_30_DAYS', customRange: [], groupBy: 'SKU', sku: '', customerCode: '', focus: 'ALL', coverageDaysThreshold: '', turnoverStatus: '', sortBy: 'PHYSICAL_DESC' })
const page = ref(1), size = ref(20), expanded = ref(false), chartsOpen = ref(true)
+const rightChart = ref<'volume' | 'daily'>('volume')
const fence = new RequestFence(), runtimeFence = new RequestFence(), trendFence = new RequestFence()
const trendOpen = ref(false), trendLoading = ref(false), daily = ref(null), trendError = ref(null)
const evidence = ref(null), evidenceOpen = ref(false)
@@ -51,7 +52,7 @@ const fields = computed(() => [
...(filters.value.groupBy === 'SKU' ? [{ key: 'sku', label: 'SKU', type: 'input' as const, width: 175, placeholder: 'SKU精确查询' }] : []),
{ key: 'focus', label: '关注条件', type: 'select', width: 165, props: { clearable: false }, options: [{ value: 'ALL', label: '全部' }, { value: 'NO_OUTBOUND_IN_WINDOW', label: '所选时段无出库' }, { value: 'COVERAGE_GT', label: '消化较慢' }, { value: 'COVERAGE_LE', label: '消化较快' }, { value: 'FROZEN', label: '有冻结库存' }, { value: 'DEFECTIVE', label: '有次品库存' }, { value: 'UNASSESSABLE', label: '待评估' }] },
{ key: 'turnoverStatus', label: '周转样本', type: 'select', width: 160, options: [{ value: '', label: '全部样本状态' }, { value: 'INSUFFICIENT', label: '周转样本不足' }] },
- { key: 'sortBy', label: '排序', type: 'select', width: 190, props: { clearable: false }, options: turnoverSortOptions.filter(({ value }) => filters.value.groupBy === 'OWNER' ? !value.startsWith('COVERAGE_') : value !== 'NO_OUTBOUND_PHYSICAL_DESC') },
+ { key: 'sortBy', label: '排序', type: 'select', width: 190, props: { clearable: false }, options: turnoverSortOptions.filter(({ value }) => filters.value.groupBy === 'OWNER' ? !value.startsWith('COVERAGE_') && !value.startsWith('UNIT_VOLUME_') : value !== 'NO_OUTBOUND_PHYSICAL_DESC') },
])
const ownerMode = computed(() => applied.value?.groupBy === 'OWNER')
const columns = computed(() => [
@@ -59,9 +60,9 @@ const columns = computed(() => [
{ prop: 'customerCode', label: '所属货主', slotName: 'owner', minWidth: 160 },
...(ownerMode.value ? [{ prop: 'physicalSkuCount', label: '有实物SKU数', minWidth: 125 }] : []),
{ prop: 'physicalQuantity', label: '实物件数', slotName: 'physicalQuantity', minWidth: 145 }, { prop: 'availableQuantity', label: '正品可售', slotName: 'availableQuantity', minWidth: 145 },
- { prop: 'inventoryVolumeM3', label: '库存立方数(m³)', slotName: 'inventoryVolume', minWidth: 200 },
- { prop: 'outboundVolumesM3', label: `${applied.value?.baseWindowDays ?? 30}日出库立方数(m³,估算)`, slotName: 'outboundVolume', minWidth: 220 },
- ...(ownerMode.value ? [] : [{ prop: 'unitVolumeM3', label: '单件体积(m³)', slotName: 'unitVolume', minWidth: 155 }]),
+ { prop: 'inventoryVolumeM3', label: '库存立方数', slotName: 'inventoryVolume', minWidth: 145 },
+ { prop: 'outboundVolumesM3', label: `${applied.value?.baseWindowDays ?? 30}日出库立方数`, slotName: 'outboundVolume', minWidth: 170 },
+ ...(ownerMode.value ? [] : [{ prop: 'unitVolumeM3', label: '单件体积', slotName: 'unitVolume', minWidth: 135 }]),
{ 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 },
@@ -81,15 +82,26 @@ async function sortColumn(prop: string) {
filters.value = { ...filters.value, sortBy }
await search()
}
-const cards = computed(() => data.value?.summary ? [
- { name: '实物件数', value: data.value.summary.physicalQuantity }, { name: '正品可售件数', value: data.value.summary.availableQuantity },
- { name: '所选时段无出库SKU', value: data.value.summary.noOutboundSkuCount }, { name: '待评估SKU', value: data.value.summary.unassessableSkuCount },
-] : [])
const incompleteDailyWindows = computed(() => data.value?.charts?.averageDaily.filter(w => w.dataStatus !== 'COMPLETE' || w.averageDailyOutboundQuantity == null) ?? [])
-const volumeCards = computed(() => data.value?.summary ? [
- { name: '库存立方数(m³)', value: data.value.summary.inventoryVolumeM3, outbound: false },
- { name: `所选 ${applied.value?.baseWindowDays ?? 30} 日出库立方数(m³,估算)`, value: data.value.summary.outboundVolumesM3.find(w => w.days === applied.value?.baseWindowDays)?.volume, outbound: true },
-] : [])
+const summaryGroups = computed(() => {
+ const summary = data.value?.summary
+ if (!summary) return []
+ const quantity = (key: string, name: string, value: TurnoverTotal, tone: string) => ({ key, name, kind: 'quantity', tone, text: turnoverNumber(value.value), note: turnoverTotalNote(value), title: undefined as string | undefined })
+ const volume = (key: string, name: string, value: TurnoverVolume | undefined, outbound: boolean, tone: string) => ({ key, name, kind: 'volume', tone, text: turnoverNumber(value?.value, 2), note: volumeNote(value, outbound, 2), title: value?.value != null ? volumeText(value.value) : undefined })
+ const days = applied.value?.baseWindowDays ?? 30
+ return [
+ { key: 'stock', name: '当前库存', note: '使用当前库存快照,不随统计时段变化', cards: [
+ quantity('physical', '实物件数', summary.physicalQuantity, 'primary'),
+ quantity('available', '正品可售件数', summary.availableQuantity, 'green'),
+ volume('inventory-volume', '库存立方数(m³)', summary.inventoryVolumeM3, false, 'purple'),
+ ] },
+ { key: 'period', name: '所选时段分析', note: `${days}天 · 截至 ${applied.value?.endDate ?? '—'}`, cards: [
+ quantity('no-outbound', '所选时段无出库SKU', summary.noOutboundSkuCount, 'orange'),
+ volume('outbound-volume', `${days}日出库立方数(m³)`, summary.outboundVolumesM3.find(w => w.days === days)?.volume, true, 'green'),
+ quantity('unassessable', '待评估SKU', summary.unassessableSkuCount, 'purple'),
+ ] },
+ ]
+})
const runtimeError = ref('')
const inventoryRuntime = computed(() => runtime.value?.warehouses.find(w => w.warehouseCode === applied.value?.warehouseCode && w.dataFamily === 'INVENTORY'))
const collectionNotice = computed(() => !data.value || runtimeError.value ? '' : inventoryCollectionNotice(inventoryRuntime.value))
@@ -252,14 +264,43 @@ onUnmounted(() => { fence.cancel(); runtimeFence.cancel(); trendFence.cancel() }
当前尚无可用库存版本,请查看数据采集。
{{ collectionNotice }};采集状态截至 {{ beijingTime(inventoryRuntime?.observedAt) }}。查看数据采集
{{ runtimeError }}
- {{ card.name }}{{ turnoverNumber(card.value.value) }}{{ turnoverTotalNote(card.value) }}
- {{ card.name }}{{ turnoverNumber(card.value?.value, 6) }}{{ volumeNote(card.value, card.outbound) }}
+
+
+ {{ group.name }}
{{ group.note }}
+
+
+ {{ card.name }}{{ card.text }}{{ card.note }}
+
+
+
+
体积优先采用完整仓库实测尺寸,缺失时按商品尺寸估算;库存按当前全品质实物件数计算,历史出库按当前商品尺寸估算,不含在途及库位空隙。缺尺寸或数量未知时展示已知小计。
{{ chartsOpen ? '收起图表' : '展开图表' }}图表与指标为{{ applied?.customerCode ? '本货主' : '本仓' }}整体,不随列表SKU或关注筛选改变
{ const owner = data?.charts?.ownerPhysicalTop[event.dataIndex]; if (owner) selectOwner(owner.customerCode) }" />暂无可排名的库存数据库存数量尚未完整确认,确认后展示货主排名
{{ data.charts.unknownOwnerCount }} 个货主未能完整计算
-
日均出库按 {{ beijingTime(data.context.analyticsContext.salesPublishedAt) }} 已发布销量版本计算;新采集数据待重算,发布后请整体刷新。SKU库存评估仍等待重算完成。
暂无完整窗口的日均出库数据,覆盖情况如下。
{{ window.days }}日:{{ window.dataStatus === 'COMPLETE' ? '日均暂不可用' : '覆盖不足' }}(完整 {{ window.completeDayCount }} / {{ window.days }} 日)
+
+
+
+ { const owner = data?.charts?.ownerVolumeTop[event.dataIndex]; if (owner) selectOwner(owner.customerCode) }" />
+ 暂无可排名的库存体积数据需有可用库存及有效尺寸;未知体积不按零参与排名
+ 当前库存,不随统计时段变化;尺寸口径见明细。{{ data.charts.unknownVolumeOwnerCount }} 个货主因缺尺寸或库存数量未知,未参与排名。
+
+
+ 日均出库按 {{ beijingTime(data.context.analyticsContext.salesPublishedAt) }} 已发布销量版本计算;新采集数据待重算,发布后请整体刷新。SKU库存评估仍等待重算完成。
暂无完整窗口的日均出库数据,覆盖情况如下。
{{ window.days }}日:{{ window.dataStatus === 'COMPLETE' ? '日均暂不可用' : '覆盖不足' }}(完整 {{ window.completeDayCount }} / {{ window.days }} 日)
+
+