fix: preserve turnover deep-link snapshots and clarify data status
This commit is contained in:
@@ -26,6 +26,15 @@ async function change(w: ReturnType<typeof mount>, patch: object, search = false
|
||||
beforeEach(() => { vi.clearAllMocks(); 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('honors an explicit new route token in the same scope and reloads its overview', async () => {
|
||||
const w = page(); await flushPromises()
|
||||
expect(mocks.turnover.mock.calls.at(-1)![0].snapshotContext).toBeUndefined()
|
||||
mocks.route.query = { endDate: '2026-10-12', warehouseCode: 'A', snapshotContext: 'token-T2' }
|
||||
await flushPromises()
|
||||
expect(mocks.turnover.mock.calls.at(-1)![0]).toMatchObject({ warehouseCode: 'A', snapshotContext: 'token-T2', includeOverview: true })
|
||||
w.findComponent(table).vm.$emit('update:page', 2); await flushPromises()
|
||||
expect(mocks.turnover.mock.calls.at(-1)![0]).toMatchObject({ snapshotContext: 'token-T2', includeOverview: false, current: 2 })
|
||||
})
|
||||
it('loads the first warehouse once and keeps charts/context across filtered pagination', async () => {
|
||||
const w = page(); await flushPromises()
|
||||
expect(mocks.turnover).toHaveBeenCalledTimes(1)
|
||||
|
||||
@@ -11,11 +11,11 @@ import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { analyticsApi } from '../api/analytics'
|
||||
import { RequestFence, beijingTime, contextParams, reasonsText } from '../state'
|
||||
import { RequestFence, beijingTime, contextParams } from '../state'
|
||||
import type { DailyResponse, Overview } from '../types'
|
||||
import type { TurnoverQuery, TurnoverResponse, TurnoverRow, TurnoverSort, TurnoverWindow } from '../turnoverTypes'
|
||||
import { turnoverRoute, validateTurnover, yesterdayBeijing, turnoverSorts } from '../turnoverNavigation'
|
||||
import { totalText, turnoverChart, turnoverNumber, windowText } from '../turnoverPresentation'
|
||||
import { totalText, turnoverChart, turnoverNumber, windowText, turnoverReasonsText as reasonsText } from '../turnoverPresentation'
|
||||
import AnalyticsChart from './AnalyticsChart.vue'
|
||||
import ChartDetailsDialog from './ChartDetailsDialog.vue'
|
||||
import SkuTrend from './SkuTrend.vue'
|
||||
@@ -90,9 +90,9 @@ async function load(refresh = false) {
|
||||
if (!warehouses.value.some(w => w.value === q.warehouseCode)) throw new Error('指定仓库不可用,请重新选择有权限的仓库')
|
||||
} catch (cause) { loading.value = false; inputError.value = (cause as Error).message; return }
|
||||
const sameScope = applied.value && scopeKey(applied.value) === scopeKey(q)
|
||||
q.snapshotContext = refresh ? undefined : sameScope ? data.value?.context.snapshotContext : linkedSnapshot
|
||||
q.snapshotContext = refresh ? undefined : linkedSnapshot ?? (sameScope ? data.value?.context.snapshotContext : undefined)
|
||||
linkedSnapshot = undefined
|
||||
q.includeOverview = refresh || !data.value?.summary || !applied.value || overviewKey(applied.value) !== overviewKey(q)
|
||||
q.includeOverview = refresh || q.snapshotContext !== data.value?.context.snapshotContext || !data.value?.summary || !applied.value || overviewKey(applied.value) !== overviewKey(q)
|
||||
if (!q.snapshotContext) { q.current = 1; page.value = 1; q.includeOverview = true }
|
||||
const previous = data.value
|
||||
const request = fence.begin(); loading.value = true
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
| 页面 | 操作 | 依据与承载结论 | 布局与验证 |
|
||||
| --- | --- | --- | --- |
|
||||
| /self-operated/inventory | 库存周转/库存明细、SKU/货主 | 当前维护入口 `docs/自营业务/库存周转视图设计.md`:默认周转、默认合法仓及SKU;可选货主仅限本视图。三窗同屏、四指标、两张240px图表、关注过滤和分页;原总览严格下钻继续进入库存明细 | 概览与列表共用固定上下文,显式深链换版同时换概览;SKU趋势1120px居中弹框,样本证据1040px居中弹框;未知不补零,待人工1920×1080、100%截图确认 |
|
||||
| 自营业务全部图表 | 统一ECharts | 2026-10-13 用户要求全面替换手写图表:经营总览日趋势、库存七桶与三个榜单、SKU日趋势、采集进度环统一通过AnalyticsChart使用ECharts Canvas;移除SVG路径生成与CSS条形绘图,保留真实数量、未知/零区分及原有下钻边界 | 日期点/分类轴/柱体点击及展开表格键盘操作同资格;未知进度仅空环;坐标、tooltip、缩放及尺寸由组件配置;待人工1920×1080、100%截图确认 |
|
||||
| 库存/销量 | SKU每日出库趋势连线与坐标 | 2026-10-13 用户反馈线未连接、坐标不显示,并明确要求图表组件:保留居中弹框,改用ECharts+vue-echarts(Canvas),配置日期轴、数量轴、网格、提示框、图例说明和范围缩放;相邻有效日连接,涉及未完整日用虚线,未知或缺失日期断线 | 库存/销量/总览的SKU弹框共用组件;总览主图的连线修复同步回归;不把未知补零;待人工1920×1080、100%截图确认 |
|
||||
| /self-operated/collection | 看板配色 | 2026-10-13 用户追加当前页面与原型首页截图:采用蓝紫主色、浅色圆形装饰数量卡、绿/橙数量强调;流程与仓库结果增加色彩层次,保持原有左右布局及真实状态文字 | 仅本看板作用域内应用;颜色不代替状态或代表未知完成度;待人工1920×1080、100%截图确认 |
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { turnoverChart, turnoverNumber, windowText } from './turnoverPresentation'
|
||||
import { turnoverChart, turnoverNumber, windowText, turnoverReasonsText } from './turnoverPresentation'
|
||||
import type { TurnoverCharts, TurnoverWindowMetric } from './turnoverTypes'
|
||||
describe('turnover honest precision and charts', () => {
|
||||
it('explains missing samples and zero average stock without exposing internal statuses', () => {
|
||||
expect(turnoverReasonsText(['INSUFFICIENT'])).toBe('合格日库存样本不足')
|
||||
expect(turnoverReasonsText(['ZERO_AVERAGE_STOCK'])).toContain('每日观测未捕获存量')
|
||||
expect(turnoverReasonsText(['INVENTORY_UNKNOWN_OR_STALE'])).toContain('已过期')
|
||||
})
|
||||
it('keeps tiny positive values distinct from zero and unknown', () => {
|
||||
expect(turnoverNumber(0.000001)).toBe('<0.01')
|
||||
expect(turnoverNumber(0.01, 1)).toBe('<0.1')
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import type { Quantity } from './types'
|
||||
import type { TurnoverCharts, TurnoverTotal, TurnoverWindowMetric } from './turnoverTypes'
|
||||
import type { EChartsOption } from 'echarts'
|
||||
import { reasonsText } from './state'
|
||||
const turnoverLabels: Record<string, string> = {
|
||||
INSUFFICIENT: '合格日库存样本不足', ZERO_AVERAGE_STOCK: '每日观测未捕获存量,无法计算周转',
|
||||
INVENTORY_UNKNOWN_OR_STALE: '当前库存未知、异常或已过期', NOT_APPLICABLE: '货主不计算整体可售天数',
|
||||
SALES_OR_STOCK_UNASSESSABLE: '部分商品的销量或库存不足以判断',
|
||||
SALES_WINDOW_UNAVAILABLE: '同期销量或商品观察证据不足',
|
||||
}
|
||||
export function turnoverReasonsText(reasons: readonly string[] | null | undefined) {
|
||||
return reasons?.map(reason => turnoverLabels[reason] ?? reasonsText([reason])).join('、') || '—'
|
||||
}
|
||||
export function turnoverNumber(value: Quantity | undefined, decimals = 2) {
|
||||
if (value == null || value === '' || !Number.isFinite(Number(value))) return '—'
|
||||
const n = Number(value)
|
||||
@@ -8,6 +18,7 @@ export function turnoverNumber(value: Quantity | undefined, decimals = 2) {
|
||||
return n.toLocaleString('zh-CN', { maximumFractionDigits: decimals })
|
||||
}
|
||||
export function totalText(total: TurnoverTotal) {
|
||||
if (total.value == null && total.knownSubtotal == null && total.unknownSkuCount === 0) return turnoverReasonsText(total.reasonCodes)
|
||||
return total.value != null ? turnoverNumber(total.value) : `已知 ${turnoverNumber(total.knownSubtotal)};${total.unknownSkuCount} SKU待核验`
|
||||
}
|
||||
export function windowText(metric: TurnoverWindowMetric | undefined) {
|
||||
@@ -18,7 +29,8 @@ export function turnoverChart(charts: TurnoverCharts, kind: 'owners' | 'daily'):
|
||||
const owners = kind === 'owners'
|
||||
const rows = owners ? charts.ownerPhysicalTop : charts.averageDaily
|
||||
return {
|
||||
aria: { enabled: true }, animationDuration: 450, color: ['#6c7bea'],
|
||||
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: 24, top: 16, bottom: 20, 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: '件/日' },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { QueryContext, Quantity } from './types'
|
||||
|
||||
// Inventory-turnover integration contract. Backend implementation and HTTP acceptance are pending.
|
||||
// Fixed-version read contract; Long identifiers and totals use the service's string serializer.
|
||||
export type TurnoverWindow = 7 | 15 | 30
|
||||
export type TurnoverGroup = 'SKU' | 'OWNER'
|
||||
export type TurnoverFocus = 'ALL' | 'NO_OUTBOUND_30D' | 'COVERAGE_GT' | 'COVERAGE_LE' | 'FROZEN' | 'DEFECTIVE' | 'UNASSESSABLE'
|
||||
@@ -44,5 +44,5 @@ export interface TurnoverCharts {
|
||||
}
|
||||
export interface TurnoverResponse {
|
||||
context: TurnoverContext; summary: TurnoverOverview | null; charts: TurnoverCharts | null
|
||||
rows: TurnoverRow[]; total: number
|
||||
rows: TurnoverRow[]; total: string | number
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user