From e7411eb03f293a52cf89b99c487ad860aac5e307 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 14 Oct 2026 12:39:16 +0800 Subject: [PATCH] feat(self-operated): unify completed-day ranges and analytics sorting --- .../components/AnalyticsView.vue | 174 +++++++++++++----- .../components/InventoryTurnoverView.test.ts | 9 +- .../components/InventoryTurnoverView.vue | 43 +++-- .../self-operated/dashboardNavigation.ts | 5 +- src/modules/self-operated/inventorySorting.ts | 16 ++ src/modules/self-operated/operation-matrix.md | 10 +- .../self-operated/overviewRange.test.ts | 23 +++ src/modules/self-operated/overviewRange.ts | 26 +++ .../self-operated/pages/OverviewView.test.ts | 66 +++++-- .../self-operated/pages/OverviewView.vue | 27 ++- .../self-operated/pages/workbench.test.ts | 125 ++++++++++--- src/modules/self-operated/state.test.ts | 6 +- src/modules/self-operated/state.ts | 4 +- .../self-operated/turnoverNavigation.test.ts | 2 +- .../self-operated/turnoverNavigation.ts | 4 +- src/modules/self-operated/turnoverTypes.ts | 6 +- src/modules/self-operated/types.ts | 15 +- 17 files changed, 426 insertions(+), 135 deletions(-) create mode 100644 src/modules/self-operated/inventorySorting.ts create mode 100644 src/modules/self-operated/overviewRange.test.ts create mode 100644 src/modules/self-operated/overviewRange.ts diff --git a/src/modules/self-operated/components/AnalyticsView.vue b/src/modules/self-operated/components/AnalyticsView.vue index 2fad1123..fc0e584b 100644 --- a/src/modules/self-operated/components/AnalyticsView.vue +++ b/src/modules/self-operated/components/AnalyticsView.vue @@ -17,6 +17,9 @@ import type { BusinessQuery, DailyResponse, InventoryAttention, InventoryMetric, import { RequestFence, RuntimePoller, contextParams, hasActiveTask, label, beijingTime, reasonsText, quantityText, sameContext, shownQuantity, validateCustomRange } from '../state' import SkuTrend from './SkuTrend.vue' import { expired, inventoryExpired, parseDashboardDrilldown, previousDate, type DashboardDrilldown } from '../dashboardNavigation' +import { overviewRange, overviewRangeOptions } from '../overviewRange' +import { inventorySortColumns, inventorySortOptions } from '../inventorySorting' +import { turnoverNumber as dailyAverageText } from '../turnoverPresentation' const emit = defineEmits<{ warehouseSelected: [code: string] }>() const props = defineProps<{ mode: 'sales' | 'inventory' }>() @@ -34,10 +37,10 @@ function readDrilldown() { } readDrilldown() function beijingToday() { return new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit' }).format(new Date()) } -function initialFilters(): QueryModel { return { warehouseCode: typeof route.query.warehouseCode === 'string' ? route.query.warehouseCode : '', sku: typeof route.query.sku === 'string' ? route.query.sku : '', analysis: drilldown.value?.analysis ?? 'summary', rangePreset: drilldown.value?.rangePreset ?? 'TODAY', windowDays: 30, salesWindowDays: 30, salesWindowEndDate: previousDate(drilldown.value?.context.anchorDate ?? beijingToday()), attention: drilldown.value?.inventory.attention ?? 'ALL', sortBy: drilldown.value?.inventory.sortBy ?? 'AVAILABLE_DESC', threshold: '', customRange: drilldown.value?.startDate && drilldown.value?.endDate ? [drilldown.value.startDate, drilldown.value.endDate] : [] } } +function initialFilters(): QueryModel { return { warehouseCode: typeof route.query.warehouseCode === 'string' ? route.query.warehouseCode : '', sku: typeof route.query.sku === 'string' ? route.query.sku : '', analysis: drilldown.value?.analysis ?? 'summary', rangePreset: drilldown.value?.rangePreset ?? 'LAST_30_DAYS', windowDays: 30, salesWindowDays: 30, salesWindowEndDate: previousDate(drilldown.value?.context.anchorDate ?? beijingToday()), attention: drilldown.value?.inventory.attention ?? 'ALL', sortBy: drilldown.value?.inventory.sortBy ?? 'AVAILABLE_DESC', threshold: '', customRange: drilldown.value?.startDate && drilldown.value?.endDate ? [drilldown.value.startDate, drilldown.value.endDate] : [] } } const filters = ref(initialFilters()) const applied = ref({ sku: String(filters.value.sku) }) -const appliedWindow = ref({ salesWindowDays: 30 as 10 | 30, salesWindowEndDate: String(filters.value.salesWindowEndDate) }) +const appliedWindow = ref({ salesWindowDays: 30 as number, salesWindowEndDate: String(filters.value.salesWindowEndDate) }) const displayedWindowDays = computed(() => context.value?.calculationWindowDays ?? appliedWindow.value.salesWindowDays) const overview = ref(null) const context = ref(drilldown.value?.context ?? null) @@ -51,6 +54,7 @@ const attention = computed(() => (filters.value.attention || 'ALL') as Inventory const sortBy = computed(() => (filters.value.sortBy || 'AVAILABLE_DESC') as InventorySort) const thresholdInput = computed(() => String(filters.value.threshold ?? '')) const appliedInventory = ref<{ attention: InventoryAttention; sortBy: InventorySort; coverageDaysThreshold?: number }>({ attention: drilldown.value?.inventory.attention ?? 'ALL', sortBy: drilldown.value?.inventory.sortBy ?? 'AVAILABLE_DESC' }) +const displayedInventorySort = ref(null) const loading = ref(false) const error = ref('') const errorDetails = ref(null) @@ -58,14 +62,26 @@ const runtimeError = ref('') const newData = ref(false) const newDay = ref(false) const current = ref(1) +let changingSortPage = false +async function setSortPage(page: number) { + changingSortPage = true + current.value = page + await nextTick() + changingSortPage = false +} const size = ref(20) const total = ref(0) -const preset = computed(() => (filters.value.rangePreset || 'TODAY') as RangePreset) +type SalesPreset = RangePreset | 'LAST_3_DAYS' | 'LAST_7_DAYS' | 'LAST_15_DAYS' | 'LAST_30_DAYS' +const preset = computed(() => (filters.value.rangePreset || 'LAST_30_DAYS') as SalesPreset) const customRange = computed(() => Array.isArray(filters.value.customRange) ? filters.value.customRange : []) const customStart = computed(() => String(customRange.value[0] ?? '')) const customEnd = computed(() => String(customRange.value[1] ?? '')) type SalesRange = { rangePreset: RangePreset; startDate?: string; endDate?: string } -function draftSalesRange(): SalesRange { return { rangePreset: preset.value, ...(preset.value === 'CUSTOM' ? { startDate: customStart.value, endDate: customEnd.value } : {}) } } +function draftSalesRange(): SalesRange { + try { return overviewRange(preset.value, origin.value?.context.anchorDate ?? beijingToday(), customStart.value, customEnd.value) } + catch { return { rangePreset: 'CUSTOM', startDate: customStart.value, endDate: customEnd.value } } + +} const appliedSalesRange = ref(draftSalesRange()) const salesRangePending = computed(() => props.mode === 'sales' && tab.value === 'summary' && JSON.stringify(draftSalesRange()) !== JSON.stringify(appliedSalesRange.value)) const tab = computed(() => (filters.value.analysis || 'summary') as 'summary' | 'hot' | 'slow') @@ -81,24 +97,26 @@ const fields = computed(() => { { key: 'sku', type: 'input', label: 'SKU', width: 230, placeholder: '输入SKU编码(精确查询)' }, ] if (props.mode === 'inventory') result.push( - ...(!drilldown.value ? [{ key: 'salesWindowDays', type: 'select' as const, label: '销量窗口', width: 150, props: { clearable: false }, options: [{ value: 10, label: '10个完整自然日' }, { value: 30, label: '30个完整自然日' }] }, - { key: 'salesWindowEndDate', type: 'date' as const, label: '统计截止日(含)', width: 180, props: { clearable: false } }] : [{ key: 'customRange', type: 'daterange' as const, label: '原图统计区间', width: 300 }]), { key: 'attention', type: 'select', label: '库存关注', width: 200, props: { 'aria-label': '库存关注' }, options: [{ value: 'ALL', label: '全部库存关注' }, { value: 'NO_OUTBOUND_IN_WINDOW', label: '所选时段无出库' }, { value: 'COVERAGE_GT_THRESHOLD', label: '可售天数超过阈值' }, { value: 'FROZEN_STOCK', label: '冻结库存' }, { value: 'DEFECTIVE_STOCK', label: '次品库存' }, { value: 'UNASSESSABLE', label: '数据不足/待观察' }] }, { key: 'threshold', type: 'input', label: '可售天数阈值', width: 180, placeholder: '天数阈值(天)', props: { 'aria-label': '可售天数阈值', inputmode: 'numeric' } }, - { key: 'sortBy', type: 'select', label: '库存排序', width: 210, options: [{ value: 'AVAILABLE_DESC', label: '正品可售量从高到低' }, { value: 'COVERAGE_DAYS_DESC', label: '可售天数从高到低' }, { value: 'PHYSICAL_DESC', label: '全品质实物从高到低' }, { value: 'OUTBOUND_ASC', label: '区间出库从低到高' }] }, + { key: 'sortBy', type: 'select', label: '库存排序', width: 210, options: inventorySortOptions }, ) else { result.push({ key: 'analysis', type: 'select', label: '销量类别', width: 170, props: { clearable: false }, options: [{ value: 'summary', label: 'SKU销量' }, { value: 'hot', label: '热销Top100' }, { value: 'slow', label: '滞销Top100' }] }) - if (tab.value === 'summary') { - result.push({ key: 'rangePreset', type: 'select', label: '销量日期', width: 150, props: { clearable: false }, options: [{ value: 'TODAY', label: '今日' }, { value: 'LAST_3_DAYS', label: '近3天' }, { value: 'THIS_WEEK', label: '本周' }, { value: 'LAST_15_DAYS', label: '近15天' }, { value: 'THIS_MONTH', label: '本月' }, { value: 'CUSTOM', label: '自定义' }] }) - if (preset.value === 'CUSTOM') result.push({ key: 'customRange', type: 'daterange', label: '销量日期范围', width: 300, props: { 'start-placeholder': '开始日期', 'end-placeholder': '结束日期', 'range-separator': '至' } }) - } else if (tab.value === 'hot') { - if (drilldown.value?.analysis === 'hot') result.push({ key: 'customRange', type: 'daterange', label: '热销日期范围', width: 300 }) - else result.push({ key: 'windowDays', type: 'select', label: '热销窗口', width: 180, props: { clearable: false }, options: [{ value: 7, label: '近7个完整自然日' }, { value: 30, label: '近30个完整自然日' }] }) - } } + result.splice(2, 0, { key: 'rangePreset', type: 'select', label: '统计时间范围', width: 180, props: { clearable: false }, options: overviewRangeOptions }) + if (preset.value === 'CUSTOM') result.splice(3, 0, { key: 'customRange', type: 'daterange', label: '日期范围', width: 300, props: { disabledDate: (date: Date) => date.toLocaleDateString('en-CA') >= (origin.value?.context.anchorDate ?? beijingToday()) } }) + return drilldown.value ? result.map(field => field.key === 'warehouseCode' ? field : { ...field, props: { ...field.props, disabled: true } }) : result }) +const salesSort = ref<'QUANTITY_ASC' | 'QUANTITY_DESC'>('QUANTITY_DESC') +async function sortSalesQuantity() { + if (loading.value || !summaryMode.value) return + const previous = salesSort.value, previousPage = current.value + salesSort.value = previous === 'QUANTITY_DESC' ? 'QUANTITY_ASC' : 'QUANTITY_DESC' + await setSortPage(1) + if (await load() === false) { salesSort.value = previous; await setSortPage(previousPage) } +} const summaryMode = computed(() => props.mode === 'sales' && tab.value === 'summary') const columns = computed(() => [ ...(summaryMode.value ? [{ key: 'actions', label: '操作', width: 110, slotName: 'actions', fixed: 'left' }] : []), @@ -117,19 +135,51 @@ const columns = computed(() => [ { key: 'inventoryCoverageDays', label: '可售天数', width: 110, slotName: 'inventoryCoverageDays' }, { key: 'lastOutbound', label: '最近已观测出库', minWidth: 220, slotName: 'lastOutbound' }, { key: 'attention', label: '关注原因', minWidth: 160, slotName: 'attention' }, - ] : [{ key: 'quantity', label: summaryMode.value || tab.value === 'hot' ? '区间出库量 / 已采集量' : '完整窗口出库量', width: 200, slotName: 'quantity' }]), + ] : [{ key: 'quantity', label: summaryMode.value || tab.value === 'hot' ? '区间出库量 / 已采集量' : '完整窗口出库量', width: 200, slotName: 'quantity', ...(summaryMode.value ? { headerSlotName: 'salesSortHeader' } : {}) }]), { key: 'status', label: '数据状态', minWidth: 130, slotName: 'status' }, { key: 'reason', label: '说明', minWidth: 220, slotName: 'reason' }, -].map(column => ['quantity', 'physicalQuantity', 'availableQuantity', 'frozenQuantity', 'defectivePhysicalQuantity', 'transitQuantity', 'windowOutboundQuantity', 'averageDailyOutboundQuantity', 'inventoryCoverageDays'].includes(String(column.key)) ? { ...column, align: 'right' } : column)) +].map(column => ['quantity', ...Object.keys(inventorySortColumns)].includes(String(column.key)) ? { + ...column, align: 'right', + ...(props.mode === 'inventory' && inventorySortColumns[String(column.key)] ? { prop: String(column.key), headerSlotName: 'inventorySortHeader', width: Math.max(Number(column.width ?? 0), 140) } : {}), +} : column)) +function inventorySortDirection(prop: string) { + const metric = inventorySortColumns[prop] + return displayedInventorySort.value === `${metric}_ASC` ? 'ASC' : displayedInventorySort.value === `${metric}_DESC` ? 'DESC' : undefined +} +async function sortInventoryColumn(prop: string) { + const metric = inventorySortColumns[prop] + if (!metric || props.mode !== 'inventory' || loading.value) return + const previousSort = appliedInventory.value.sortBy, previousPage = current.value + const nextSort: InventorySort = `${metric}_${inventorySortDirection(prop) === 'DESC' ? 'ASC' : 'DESC'}` + appliedInventory.value = { ...appliedInventory.value, sortBy: nextSort } + filters.value = { ...filters.value, sortBy: nextSort } + await setSortPage(1) + if (await load() === false) { + appliedInventory.value = { ...appliedInventory.value, sortBy: previousSort } + filters.value = { ...filters.value, sortBy: previousSort } + await setSortPage(previousPage) + } +} +function inventorySalesGap(row: InventoryMetric) { + const status = row.quantityStatuses?.windowOutboundQuantity + if (status === 'UNIT_UNKNOWN_OR_CONFLICT' || row.status === 'UNIT_UNKNOWN_OR_CONFLICT') return '数量单位待核验' + if (status === 'SALES_UNTRUSTED' || context.value?.rebuildRequired) return '新采集数据待发布' + if (status === 'COVERAGE_INSUFFICIENT' || row.salesCoverage === 'COVERAGE_INSUFFICIENT') return '销量窗口未采齐' + return '暂无完整窗口数据' +} +function inventoryReasonText(row: InventoryMetric) { + const reasons = row.reasonCodes?.filter(reason => reason !== 'OBSERVATION_INSUFFICIENT') ?? [] + const observationNote = row.observationEligible === false ? '商品历史观察不足(仅提示,不影响完整窗口统计)' : '' + return [reasons.length ? reasonsText(reasons) : '', observationNote].filter(Boolean).join('、') || '—' +} +function inventoryMissingReason(row: InventoryMetric, coverage = false) { + if (coverage && row.status === 'NO_OUTBOUND_IN_WINDOW') return '所选时段无出库' + if (coverage && row.status?.startsWith('INVENTORY_')) return '当前可售库存待核验' + return inventorySalesGap(row) +} 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 } @@ -137,13 +187,14 @@ function query(): InventoryQuery { const result: InventoryQuery = { sourceAccount, warehouseCode: warehouseCode.value, sku: applied.value.sku || undefined, current: current.value, size: size.value, ...contextParams(context.value) } if (drilldown.value?.customerCode) result.customerCode = drilldown.value.customerCode if (summaryMode.value) { - Object.assign(result, appliedSalesRange.value) + Object.assign(result, appliedSalesRange.value, { salesSort: salesSort.value }) } else if (props.mode === 'sales' && tab.value === 'hot') { - if (drilldown.value?.analysis === 'hot') Object.assign(result, { rangePreset: 'CUSTOM', startDate: drilldown.value.startDate, endDate: drilldown.value.endDate }) - else result.windowDays = windowDays.value + Object.assign(result, appliedSalesRange.value) } + if (props.mode === 'sales' && tab.value === 'slow') Object.assign(result, appliedSalesRange.value) if (props.mode === 'inventory') { Object.assign(result, appliedInventory.value, drilldown.value?.inventory) + result.sortBy = appliedInventory.value.sortBy if (drilldown.value) Object.assign(result, { rangePreset: 'CUSTOM', startDate: drilldown.value.startDate, endDate: drilldown.value.endDate }) else Object.assign(result, appliedWindow.value) } @@ -151,8 +202,8 @@ function query(): InventoryQuery { } async function load(reset = false, keepContext = false) { if (!permitted.value || !warehouseCode.value || routeProblem.value) return - if (origin.value && context.value && expired(context.value)) { error.value = '原图查询已过期,请返回经营总览整体刷新'; return } - if (drilldown.value && props.mode === 'inventory' && context.value && inventoryExpired(context.value)) { error.value = '原图库存已过期,请刷新总览'; return } + if (origin.value && context.value && expired(context.value)) { error.value = '原图查询已过期,请返回经营总览整体刷新'; return false } + if (drilldown.value && props.mode === 'inventory' && context.value && inventoryExpired(context.value)) { error.value = '原图库存已过期,请刷新总览'; return false } if (summaryMode.value && !appliedSalesRange.value) return if (reset) { if (!origin.value && !keepContext) context.value = null; daily.value = null; rows.value = []; total.value = 0; newData.value = false; current.value = 1 } trendOpen.value = false @@ -172,7 +223,9 @@ async function load(reset = false, keepContext = false) { rows.value = result.records total.value = result.total daily.value = null - } catch (cause) { if (request.current()) { errorDetails.value = getApiErrorDetails(cause, '查询失败,请重新查询'); error.value = errorDetails.value.message } } + if (props.mode === 'inventory') displayedInventorySort.value = params.sortBy ?? 'AVAILABLE_DESC' + return true + } catch (cause) { if (request.current()) { errorDetails.value = getApiErrorDetails(cause, '查询失败,请重新查询'); error.value = errorDetails.value.message; return false } } finally { if (request.current()) loading.value = false } } async function showTrend(row: SalesSummary | Metric | InventoryMetric) { @@ -216,26 +269,24 @@ watch(trendOpen, open => { if (!open) { dailyFence.cancel(); daily.value = null; function visibility() { poller.setVisible(!document.hidden) } function clearSalesResults() { dataFence.cancel(); dailyFence.cancel(); trendOpen.value = false; daily.value = null; rows.value = []; total.value = 0; loading.value = false; error.value = ''; errorDetails.value = null } function applySalesRange(anchor?: string) { - const problem = preset.value === 'CUSTOM' ? validateCustomRange(customStart.value, customEnd.value, anchor) : '' - if (problem) { clearSalesResults(); appliedSalesRange.value = null; error.value = problem; return false } - appliedSalesRange.value = draftSalesRange(); return true + try { appliedSalesRange.value = overviewRange(preset.value, anchor ?? beijingToday(), customStart.value, customEnd.value); return true } + catch (cause) { clearSalesResults(); appliedSalesRange.value = null; error.value = (cause as Error).message; return false } } + function search() { - if (summaryMode.value && !applySalesRange(origin.value?.context.anchorDate ?? beijingToday())) return + if (!applySalesRange(origin.value?.context.anchorDate ?? beijingToday())) return let nextWindow = appliedWindow.value if (props.mode === 'inventory' && !drilldown.value) { - const end = String(filters.value.salesWindowEndDate ?? '') - const days = Number(filters.value.salesWindowDays) - const problem = validateCustomRange(end, end, previousDate(origin.value?.context.anchorDate ?? beijingToday())) - if (problem || ![10, 30].includes(days)) { error.value = problem || '请选择10天或30天销量窗口'; return } - nextWindow = { salesWindowDays: days as 10 | 30, salesWindowEndDate: end } + const range = appliedSalesRange.value! + nextWindow = { salesWindowDays: Math.round((Date.parse(range.endDate!) - Date.parse(range.startDate!)) / 86400000) + 1, salesWindowEndDate: range.endDate! } } + const rawThreshold = thresholdInput.value.trim() if (props.mode === 'inventory' && ((rawThreshold && (!/^\d+$/.test(rawThreshold) || !Number.isSafeInteger(Number(rawThreshold)) || Number(rawThreshold) <= 0)) || (attention.value === 'COVERAGE_GT_THRESHOLD' && !rawThreshold))) { error.value = '可售天数阈值须为正整数;选择超过阈值关注时必须填写' return } - const keepContext = props.mode === 'inventory' && nextWindow.salesWindowDays === appliedWindow.value.salesWindowDays && nextWindow.salesWindowEndDate === appliedWindow.value.salesWindowEndDate && sortBy.value !== appliedInventory.value.sortBy + const keepContext = !newData.value && !newDay.value && props.mode === 'inventory' && nextWindow.salesWindowDays === appliedWindow.value.salesWindowDays && nextWindow.salesWindowEndDate === appliedWindow.value.salesWindowEndDate && sortBy.value !== appliedInventory.value.sortBy appliedWindow.value = nextWindow appliedInventory.value = { attention: attention.value, sortBy: sortBy.value, coverageDaysThreshold: rawThreshold ? Number(rawThreshold) : undefined } applied.value = { sku: String(filters.value.sku ?? '').trim() } @@ -243,6 +294,7 @@ function search() { void load(true, keepContext) } async function reset() { + salesSort.value = 'QUANTITY_DESC' applyingRoute = true drilldown.value = null; origin.value = null filters.value = { ...initialFilters(), warehouseCode: warehouseCode.value || selectedWarehouse || defaultWarehouse(), sku: '' } @@ -255,7 +307,7 @@ function returnOverview() { } async function clearDrilldown() { drilldown.value = null - filters.value = { ...filters.value, sku: '', analysis: 'summary', rangePreset: 'TODAY', windowDays: 30, attention: 'ALL', sortBy: 'AVAILABLE_DESC', threshold: '', customRange: [] } + filters.value = { ...filters.value, sku: '', analysis: 'summary', rangePreset: 'LAST_30_DAYS', windowDays: 30, attention: 'ALL', sortBy: 'AVAILABLE_DESC', threshold: '', customRange: [] } appliedSalesRange.value = draftSalesRange() applied.value = { sku: '' }; appliedInventory.value = { attention: 'ALL', sortBy: 'AVAILABLE_DESC' }; current.value = 1 await nextTick(); void load() @@ -263,15 +315,16 @@ async function clearDrilldown() { function link(path: string) { void router.push({ path, query: { warehouseCode: warehouseCode.value, sku: applied.value.sku } }) } let applyingRoute = false watch(warehouseCode, () => { if (applyingRoute) return; drilldown.value = null; origin.value = null; trendOpen.value = false; dataFence.cancel(); dailyFence.cancel(); rows.value = []; total.value = 0; context.value = null; daily.value = null; error.value = ''; current.value = 1; void load(true) }) -watch([() => filters.value.salesWindowDays, () => filters.value.salesWindowEndDate], () => { if (!applyingRoute && props.mode === 'inventory' && !drilldown.value) search() }) -watch([customStart, customEnd], () => { if (applyingRoute || !summaryMode.value) return; error.value = ''; errorDetails.value = null }) -watch([preset, tab, windowDays], () => { + +watch([customStart, customEnd], () => { if (applyingRoute) return; error.value = ''; errorDetails.value = null }) +watch([preset, tab], () => { if (applyingRoute) return + if (props.mode === 'inventory') { search(); return } clearSalesResults() - if (summaryMode.value && !applySalesRange(context.value?.anchorDate ?? beijingToday())) return + if (!applySalesRange(origin.value?.context.anchorDate ?? context.value?.anchorDate ?? beijingToday())) return current.value = 1; void load() }) -watch([current, size], () => { if (!applyingRoute) void load() }) +watch([current, size], () => { if (!applyingRoute && !changingSortPage) void load() }) onMounted(() => { visibility(); poller.start(); document.addEventListener('visibilitychange', visibility); void load() }) watch(() => route.query, async () => { if (route.path && route.path !== `/self-operated/${props.mode}`) return @@ -292,13 +345,13 @@ onUnmounted(() => { poller.stop(); dataFence.cancel(); dailyFence.cancel(); docu + + @@ -326,8 +389,15 @@ onUnmounted(() => { poller.stop(); dataFence.cancel(); dailyFence.cancel(); docu - - + + + + @@ -342,6 +412,12 @@ onUnmounted(() => { poller.stop(); dataFence.cancel(); dailyFence.cancel(); docu