fix: keep analytics date ranges together and apply atomically

This commit is contained in:
2026-10-13 13:43:16 +08:00
parent 6bdaff3fdf
commit 1de73b5f1c
5 changed files with 147 additions and 27 deletions
@@ -24,13 +24,14 @@ const router = useRouter()
const access = useAccessPreviewStore()
const permitted = computed(() => access.can('platform:self-operated-analytics:list'))
const sourceAccount = 'lingxing-primary'
const filters = ref<QueryModel>({ warehouseCode: typeof route.query.warehouseCode === 'string' ? route.query.warehouseCode : '', rangePreset: 'LAST_15_DAYS', startDate: '', endDate: '' })
const filters = ref<QueryModel>({ warehouseCode: typeof route.query.warehouseCode === 'string' ? route.query.warehouseCode : '', rangePreset: 'LAST_15_DAYS', dateRange: [] })
let selectedWarehouse = String(filters.value.warehouseCode)
const context = ref<QueryContext | null>(null)
const data = ref<Dashboard | null>(null)
const runtime = ref<Overview | null>(null)
const loading = ref(false)
const error = ref('')
const rangeValidationError = ref('')
const errorDetails = ref<ApiErrorDetails | null>(null)
const runtimeError = ref('')
const routeError = ref('')
@@ -45,7 +46,7 @@ try {
if (typeof route.query.context !== 'string' || typeof route.query.overviewReturn !== 'string' || !filters.value.warehouseCode) throw new Error('经营总览返回参数无效')
context.value = parseContext(route.query.context)
appliedRange = parseOverviewReturn(route.query.overviewReturn)
Object.assign(filters.value, { rangePreset: appliedRange.rangePreset, startDate: appliedRange.startDate ?? '', endDate: appliedRange.endDate ?? '' })
Object.assign(filters.value, { rangePreset: appliedRange.rangePreset, dateRange: appliedRange.startDate && appliedRange.endDate ? [appliedRange.startDate, appliedRange.endDate] : [] })
restoreScroll = appliedRange.scroll
}
} catch (cause) { routeError.value = cause instanceof Error ? cause.message : '经营总览返回参数无效' }
@@ -55,7 +56,7 @@ const stockExpired = computed(() => Boolean(context.value && inventoryExpired(co
const fields = computed<QueryField[]>(() => [
{ key: 'warehouseCode', type: 'select', label: '仓库', width: 220, placeholder: '切换仓库', props: { clearable: false }, options: [...new Map((runtime.value?.warehouses ?? []).map(item => [item.warehouseCode, { value: item.warehouseCode, label: item.warehouseName || item.warehouseCode }])).values()] },
{ key: 'rangePreset', type: 'select', label: '出库趋势范围', width: 180, props: { clearable: false }, options: rangeOptions },
...(filters.value.rangePreset === 'CUSTOM' ? [{ key: 'startDate', type: 'date' as const, label: '开始日期', width: 160 }, { key: 'endDate', type: 'date' as const, label: '结束日期', width: 160 }] : []),
...(filters.value.rangePreset === 'CUSTOM' ? [{ key: 'dateRange', type: 'daterange' as const, label: '日期范围', width: 300 }] : []),
])
const metricNames: { key: keyof Dashboard['kpis']; label: string; inventory?: boolean }[] = [
{ key: 'intervalOutboundQuantity', label: '区间出库量' }, { key: 'averageDailyOutboundQuantity30d', label: '近30日日均出库' },
@@ -82,13 +83,14 @@ async function load(refresh = false) {
if (!refresh && contextExpired.value) { error.value = '原图查询已过期,请整体刷新经营总览'; return }
const range = String(filters.value.rangePreset) as RangePreset
if (!rangeOptions.some(option => option.value === range)) { error.value = '请选择有效的出库趋势范围'; return }
const start = String(filters.value.startDate ?? '')
const end = String(filters.value.endDate ?? '')
if (range === 'CUSTOM') { const problem = validateCustomRange(start, end, context.value?.anchorDate); if (problem) { error.value = problem; return } }
const selected = Array.isArray(filters.value.dateRange) ? filters.value.dateRange : []
const start = typeof selected[0] === 'string' ? selected[0] : ''
const end = typeof selected[1] === 'string' ? selected[1] : ''
if (range === 'CUSTOM') { const problem = validateCustomRange(start, end, context.value?.anchorDate); if (problem) { rangeValidationError.value = problem; error.value = problem; errorDetails.value = null; return } }
const pinned = refresh ? null : context.value
const request = dataFence.begin()
trendOpen.value = false
loading.value = true; error.value = ''; errorDetails.value = null
loading.value = true; error.value = ''; rangeValidationError.value = ''; errorDetails.value = null
try {
const params: DashboardQuery = { sourceAccount, warehouseCode: warehouseCode.value, rangePreset: range, ...contextParams(pinned), ...(range === 'CUSTOM' ? { startDate: start, endDate: end } : {}) }
const result = await analyticsApi.dashboard(params, request.signal)
@@ -144,7 +146,7 @@ async function showTrend(item: DashboardItem) {
finally { if (request.current()) trendLoading.value = false }
}
async function reset() {
filters.value = { warehouseCode: warehouseCode.value || selectedWarehouse || runtime.value?.warehouses.find(item => item.warehouseCode)?.warehouseCode || '', rangePreset: 'LAST_15_DAYS', startDate: '', endDate: '' }
filters.value = { warehouseCode: warehouseCode.value || selectedWarehouse || runtime.value?.warehouses.find(item => item.warehouseCode)?.warehouseCode || '', rangePreset: 'LAST_15_DAYS', dateRange: [] }
routeError.value = ''
await nextTick()
void load(true)
@@ -153,6 +155,12 @@ watch(warehouseCode, code => {
dataFence.cancel(); trendFence.cancel(); data.value = null; context.value = null; trendOpen.value = false; loading.value = false; error.value = ''; errorDetails.value = null; newData.value = false; routeError.value = ''; restoreScroll = null
if (code) { selectedWarehouse = code; void load(true) }
})
watch(() => [filters.value.rangePreset, filters.value.dateRange], () => {
if (rangeValidationError.value) {
if (error.value === rangeValidationError.value) error.value = ''
rangeValidationError.value = ''
}
}, { deep: true })
watch(trendOpen, open => { if (!open) { trendFence.cancel(); trend.value = null; trendLoading.value = false } })
onMounted(() => { visibility(); poller.start(); timer = setInterval(() => { now.value = Date.now() }, 1000); document.addEventListener('visibilitychange', visibility); if (context.value && !routeError.value) void load() })
onUnmounted(() => { dataFence.cancel(); trendFence.cancel(); poller.stop(); clearInterval(timer); document.removeEventListener('visibilitychange', visibility) })