fix(self-operated): clarify today's unavailable sales and tooltip precision
This commit is contained in:
@@ -124,6 +124,12 @@ const columns = computed<DataTableColumn[]>(() => [
|
||||
const dataFence = new RequestFence()
|
||||
const dailyFence = new RequestFence()
|
||||
const runtimeFence = new RequestFence()
|
||||
function showRecentCompleteDays() {
|
||||
const end = previousDate(context.value?.anchorDate ?? beijingToday())
|
||||
const start = new Date(Date.parse(`${end}T00:00:00Z`) - 29 * 86400000).toISOString().slice(0, 10)
|
||||
filters.value = { ...filters.value, rangePreset: 'CUSTOM', customRange: [start, end] }
|
||||
void search()
|
||||
}
|
||||
function asSummary(row: unknown) { return row as SalesSummary }
|
||||
function asMetric(row: unknown) { return row as Metric }
|
||||
function asInventory(row: unknown) { return row as InventoryMetric }
|
||||
@@ -286,6 +292,7 @@ onUnmounted(() => { poller.stop(); dataFence.cancel(); dailyFence.cancel(); docu
|
||||
<template v-if="$slots['view-tabs']" #toolbar-start><slot name="view-tabs" /></template>
|
||||
<template #filters><QueryToolbar v-model="filters" :fields="fields" ariaLabel="自营SKU筛选" :preference-key="`self-operated-${mode}-query`" :loading="loading" :search-on-reset="false" @search="search" @reset="reset" /></template>
|
||||
<template #notices>
|
||||
<p v-if="summaryMode && !drilldown && appliedSalesRange?.rangePreset === 'TODAY' && rows.length && rows.every(row => shownQuantity(asSummary(row)) == null)" class="analytics-note" role="status">当前仅查询今日,所列商品暂无可用的今日数量;历史采集量不计入今日。<el-button link type="primary" @click="showRecentCompleteDays">查看近30个完整日</el-button></p>
|
||||
<p v-if="routeProblem" role="alert">{{ routeProblem }};已停止查询,不能按全仓条件继续。<el-button @click="returnOverview">返回经营总览</el-button></p>
|
||||
<div v-if="origin" class="density-actions"><span>{{ drilldown?.summary ?? '来自经营总览 · 来源筛选已清除,保留原有效版本' }}</span><el-button v-if="drilldown" @click="clearDrilldown">清除来源条件</el-button><el-button @click="returnOverview">返回经营总览</el-button></div>
|
||||
<p v-if="runtimeError" class="analytics-note" role="status">{{ runtimeError }};最近观测:{{ beijingTime(overview?.observedAt) }}</p>
|
||||
@@ -317,7 +324,7 @@ onUnmounted(() => { poller.stop(); dataFence.cancel(); dailyFence.cancel(); docu
|
||||
<template #unit="{ row }">{{ asMetric(row).quantityUnit === 'PCS' ? '件' : asMetric(row).quantityUnit || '—' }}</template>
|
||||
<template #lastOutbound="{ row }">{{ asMetric(row).lastObservedOutboundDate ?? '已采集历史内未见出库' }}<br />历史覆盖起点:{{ asMetric(row).observedHistoryStartDate ?? '—' }}</template>
|
||||
<template #attention="{ row }">{{ reasonsText(asMetric(row).attentionReasons) }}</template>
|
||||
<template #quantity="{ row }">{{ quantityText(summaryMode ? shownQuantity(asSummary(row)) : asMetric(row).outboundQuantity) }} {{ (summaryMode ? asSummary(row).dataStatus !== 'COMPLETE' : tab === 'hot' && asMetric(row).salesCoverage !== 'COMPLETE') ? '(已采集,未完整)' : '' }}</template>
|
||||
<template #quantity="{ row }">{{ quantityText(summaryMode ? shownQuantity(asSummary(row)) : asMetric(row).outboundQuantity) }} {{ summaryMode && shownQuantity(asSummary(row)) == null ? '(暂无可用数据)' : (summaryMode ? asSummary(row).dataStatus !== 'COMPLETE' : tab === 'hot' && asMetric(row).salesCoverage !== 'COMPLETE') ? '(已采集,未完整)' : '' }}</template>
|
||||
<template #status="{ row }">{{ label(summaryMode ? asSummary(row).dataStatus : asMetric(row).status) }}</template>
|
||||
<template #reason="{ row }">{{ reasonsText(summaryMode ? asSummary(row).reasonCodes : asMetric(row).reasonCodes) }}</template>
|
||||
<template v-for="key in ['physicalQuantity', 'availableQuantity', 'frozenQuantity', 'defectivePhysicalQuantity', 'transitQuantity', 'windowOutboundQuantity', 'averageDailyOutboundQuantity', 'inventoryCoverageDays'] as const" :key="key" #[key]="{ row }">{{ quantityText(asInventory(row)[key]) }}<span v-if="asMetric(row).quantityStatuses?.[key] && asMetric(row).quantityStatuses?.[key] !== 'VALID'">({{ label(asMetric(row).quantityStatuses?.[key]) }})</span></template>
|
||||
|
||||
@@ -115,3 +115,5 @@
|
||||
| 数据采集/当前阶段圆环动效 | 用户反馈圆环跳闪、未知总量看似停止 | ECharts Canvas圆环单独启用合并更新及700ms进度过渡;未知总量仅在可见、状态有效且执行中显示旋转指示及执行中文字,不伪造比例;等待、陈旧、隐藏或阶段结束停止动效,遵循减少动态效果偏好 | 状态与轮询回归;视觉待人工截图确认 |
|
||||
|
||||
| 经营总览/统一统计时间(2026-10-14) | 用户要求分布、预计消化最长及无出库库存榜跟随顶部搜索时间 | 三榜、分布及日均/无出库指标统一使用所选区间;名称改为“所选时段无出库库存Top10”,明细保留原日期并锁定;库存取当前版本,缺日不填零,30/60/90天分组边界不变 | 日期、分组、排序与下钻回归;待人工1920×1080、100%截图确认 |
|
||||
|
||||
| 销量分析/今日空数量及日均精度 | 用户反馈历史采齐但今日为空、日均悬浮小数过长 | 今日全无可用数量时明确范围并提供近30完整日查询;空值不标已采集。日均悬浮与摘要统一最多2位小数、件/日单位,底层精度不变 | 日期请求与精度回归;实际服务覆盖需返回值核对 |
|
||||
|
||||
@@ -50,6 +50,14 @@ beforeEach(() => {
|
||||
})
|
||||
afterEach(() => { wrappers.splice(0).forEach(wrapper => wrapper.unmount()); vi.useRealTimers() })
|
||||
describe('self-operated actual workbench request state', () => {
|
||||
it('explains unknown today values and queries the preceding 30 complete days explicitly', async () => {
|
||||
mocks.api.summary.mockResolvedValue({ context, records: [{ ...summary, observedQuantity: null, dataStatus: 'UNKNOWN' }], total: 1 })
|
||||
const wrapper = mount(AnalyticsView, { ...options, props: { mode: 'sales' } }); wrappers.push(wrapper); await flushPromises()
|
||||
expect(wrapper.text()).toContain('暂无可用数据')
|
||||
expect(wrapper.text()).not.toContain('(已采集,未完整)')
|
||||
await findButton(wrapper, '查看近30个完整日').trigger('click'); await flushPromises()
|
||||
expect(mocks.api.summary.mock.calls.at(-1)?.[0]).toMatchObject({ rangePreset: 'CUSTOM', startDate: '2026-09-12', endDate: '2026-10-11' })
|
||||
})
|
||||
it.each(['sales', 'inventory'] as const)('automatically loads the first available warehouse on %s entry', async mode => {
|
||||
mocks.routeQuery = {}
|
||||
mocks.api.overview.mockResolvedValue({ ...overview, warehouses: [{ warehouseCode: 'WH-B' }, { warehouseCode: 'WH-A' }] })
|
||||
|
||||
@@ -16,6 +16,10 @@ describe('turnover honest precision and charts', () => {
|
||||
const partial: TurnoverWindowMetric = { days: 15, startDate: '2026-09-28', endDateExclusive: '2026-10-13', dataStatus: 'PARTIAL', observedQuantity: 150, outboundQuantity: null, averageDailyOutboundQuantity: null, completeDayCount: 7, partialDayCount: 1, unknownDayCount: 7, reasonCodes: [] }
|
||||
const charts: TurnoverCharts = { ownerPhysicalTop: [], unknownOwnerCount: 0, averageDaily: [{ ...partial, days: 7, dataStatus: 'COMPLETE', averageDailyOutboundQuantity: 10 }, partial] }
|
||||
const option = turnoverChart(charts, 'daily')
|
||||
const tooltip = option.tooltip as { valueFormatter: (value: number | null) => string }
|
||||
expect(tooltip.valueFormatter(2165.133333)).toBe('2,165.13 件/日')
|
||||
expect(tooltip.valueFormatter(0)).toBe('0 件/日')
|
||||
expect(tooltip.valueFormatter(null)).toBe('— 件/日')
|
||||
expect(JSON.stringify(option.series)).toContain('"data":[10,null]')
|
||||
expect(windowText(partial)).toBe('已采集 150,覆盖不足')
|
||||
})
|
||||
|
||||
@@ -47,7 +47,7 @@ export function turnoverChart(charts: TurnoverCharts, kind: 'owners' | 'daily'):
|
||||
return {
|
||||
aria: { enabled: true }, animation: typeof window === 'undefined' || !window.matchMedia?.('(prefers-reduced-motion: reduce)').matches,
|
||||
animationDuration: 450, color: ['#6c7bea'],
|
||||
tooltip: { trigger: 'axis', renderMode: 'richText' }, grid: { left: 12, right: 40, top: owners ? 12 : 36, bottom: 12, containLabel: true },
|
||||
tooltip: { trigger: 'axis', renderMode: 'richText', valueFormatter: value => `${turnoverNumber(typeof value === 'number' || typeof value === 'string' ? value : null)} ${owners ? '件' : '件/日'}` }, grid: { left: 12, right: 40, top: owners ? 12 : 36, bottom: 12, containLabel: true },
|
||||
xAxis: owners ? { type: 'value', name: '件' } : { type: 'category', data: charts.averageDaily.map(w => `${w.days}日`) },
|
||||
yAxis: owners ? { type: 'category', inverse: true, data: charts.ownerPhysicalTop.map(w => w.customerName || w.customerCode) } : { type: 'value', name: '件/日', nameGap: 12, nameTextStyle: { align: 'left' } },
|
||||
series: [{ type: 'bar', barMaxWidth: 24, itemStyle: { borderRadius: 4 }, data: rows.map((_, i) => {
|
||||
|
||||
Reference in New Issue
Block a user