update dashboard store
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
import { useDashboardStore } from '../dashboard'
|
||||
|
||||
vi.mock('@/utils/request', () => ({
|
||||
api: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
put: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
const mockOverview = {
|
||||
today: { success: 10, failed: 2 },
|
||||
this_week: { success: 50, failed: 5 },
|
||||
this_month: { success: 200, failed: 20 },
|
||||
by_type: [
|
||||
{ data_type: 'order', success: 80, failed: 10 },
|
||||
{ data_type: 'product', success: 60, failed: 5 },
|
||||
{ data_type: 'refund', success: 40, failed: 3 },
|
||||
{ data_type: 'inventory', success: 20, failed: 2 },
|
||||
],
|
||||
}
|
||||
|
||||
const mockTrendData = [
|
||||
{ date: '2026-03-18', success: 30, failed: 2 },
|
||||
{ date: '2026-03-19', success: 25, failed: 3 },
|
||||
{ date: '2026-03-20', success: 40, failed: 1 },
|
||||
]
|
||||
|
||||
const mockBreakdownData = [
|
||||
{ id: 1, name: 'Acme Corp', success: 100, failed: 10 },
|
||||
{ id: 2, name: 'Beta Inc', success: 80, failed: 5 },
|
||||
]
|
||||
|
||||
describe('useDashboardStore', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('initial state', () => {
|
||||
it('S1: starts with correct initial state', () => {
|
||||
const store = useDashboardStore()
|
||||
expect(store.overview).toBeNull()
|
||||
expect(store.overviewLoading).toBe(false)
|
||||
expect(store.trendData).toEqual([])
|
||||
expect(store.trendGroupBy).toBe('day')
|
||||
expect(store.breakdownData).toEqual([])
|
||||
expect(store.breakdownDimension).toBe('company')
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetchOverview', () => {
|
||||
it('S2: fetches overview data successfully', async () => {
|
||||
const { api } = await import('@/utils/request')
|
||||
vi.mocked(api.get).mockResolvedValueOnce(mockOverview)
|
||||
|
||||
const store = useDashboardStore()
|
||||
await store.fetchOverview()
|
||||
|
||||
expect(api.get).toHaveBeenCalledWith('/api/v1/dashboard/overview')
|
||||
expect(store.overview).toEqual(mockOverview)
|
||||
expect(store.overviewLoading).toBe(false)
|
||||
expect(store.overviewError).toBe('')
|
||||
})
|
||||
|
||||
it('S3: handles overview fetch error', async () => {
|
||||
const { api } = await import('@/utils/request')
|
||||
vi.mocked(api.get).mockRejectedValueOnce(new Error('网络错误'))
|
||||
|
||||
const store = useDashboardStore()
|
||||
await store.fetchOverview()
|
||||
|
||||
expect(store.overview).toBeNull()
|
||||
expect(store.overviewError).toBe('网络错误')
|
||||
expect(store.overviewLoading).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetchTrend', () => {
|
||||
it('S4: fetches trend data with default params', async () => {
|
||||
const { api } = await import('@/utils/request')
|
||||
vi.mocked(api.get).mockResolvedValueOnce(mockTrendData)
|
||||
|
||||
const store = useDashboardStore()
|
||||
await store.fetchTrend()
|
||||
|
||||
expect(api.get).toHaveBeenCalledWith('/api/v1/dashboard/trend', expect.objectContaining({
|
||||
group_by: 'day',
|
||||
}))
|
||||
expect(store.trendData).toEqual(mockTrendData)
|
||||
})
|
||||
|
||||
it('S5: passes filter params to trend API', async () => {
|
||||
const { api } = await import('@/utils/request')
|
||||
vi.mocked(api.get).mockResolvedValueOnce(mockTrendData)
|
||||
|
||||
const store = useDashboardStore()
|
||||
store.trendGroupBy = 'week'
|
||||
store.trendDataType = 'order'
|
||||
store.trendFrom = '2026-03-01'
|
||||
store.trendTo = '2026-03-20'
|
||||
await store.fetchTrend()
|
||||
|
||||
expect(api.get).toHaveBeenCalledWith('/api/v1/dashboard/trend', expect.objectContaining({
|
||||
group_by: 'week',
|
||||
data_type: 'order',
|
||||
from: '2026-03-01',
|
||||
to: '2026-03-20',
|
||||
}))
|
||||
})
|
||||
|
||||
it('S6: handles trend fetch error', async () => {
|
||||
const { api } = await import('@/utils/request')
|
||||
vi.mocked(api.get).mockRejectedValueOnce(new Error('趋势接口超时'))
|
||||
|
||||
const store = useDashboardStore()
|
||||
await store.fetchTrend()
|
||||
|
||||
expect(store.trendData).toEqual([])
|
||||
expect(store.trendError).toBe('趋势接口超时')
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetchBreakdown', () => {
|
||||
it('S7: fetches breakdown data with default params', async () => {
|
||||
const { api } = await import('@/utils/request')
|
||||
vi.mocked(api.get).mockResolvedValueOnce(mockBreakdownData)
|
||||
|
||||
const store = useDashboardStore()
|
||||
await store.fetchBreakdown()
|
||||
|
||||
expect(api.get).toHaveBeenCalledWith('/api/v1/dashboard/breakdown', expect.objectContaining({
|
||||
dimension: 'company',
|
||||
}))
|
||||
expect(store.breakdownData).toEqual(mockBreakdownData)
|
||||
})
|
||||
|
||||
it('S8: passes filter params to breakdown API', async () => {
|
||||
const { api } = await import('@/utils/request')
|
||||
vi.mocked(api.get).mockResolvedValueOnce(mockBreakdownData)
|
||||
|
||||
const store = useDashboardStore()
|
||||
store.breakdownDimension = 'store'
|
||||
store.breakdownDataType = 'refund'
|
||||
store.breakdownFrom = '2026-03-01'
|
||||
store.breakdownTo = '2026-03-20'
|
||||
await store.fetchBreakdown()
|
||||
|
||||
expect(api.get).toHaveBeenCalledWith('/api/v1/dashboard/breakdown', expect.objectContaining({
|
||||
dimension: 'store',
|
||||
data_type: 'refund',
|
||||
from: '2026-03-01',
|
||||
to: '2026-03-20',
|
||||
}))
|
||||
})
|
||||
|
||||
it('S9: handles breakdown fetch error', async () => {
|
||||
const { api } = await import('@/utils/request')
|
||||
vi.mocked(api.get).mockRejectedValueOnce(new Error('分维度接口失败'))
|
||||
|
||||
const store = useDashboardStore()
|
||||
await store.fetchBreakdown()
|
||||
|
||||
expect(store.breakdownData).toEqual([])
|
||||
expect(store.breakdownError).toBe('分维度接口失败')
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetchAll', () => {
|
||||
it('S10: calls all three fetch functions in parallel', async () => {
|
||||
const { api } = await import('@/utils/request')
|
||||
vi.mocked(api.get).mockImplementation((url: string) => {
|
||||
if (url.includes('overview')) return Promise.resolve(mockOverview) as never
|
||||
if (url.includes('trend')) return Promise.resolve(mockTrendData) as never
|
||||
if (url.includes('breakdown')) return Promise.resolve(mockBreakdownData) as never
|
||||
return Promise.resolve(null) as never
|
||||
})
|
||||
|
||||
const store = useDashboardStore()
|
||||
await store.fetchAll()
|
||||
|
||||
expect(store.overview).toEqual(mockOverview)
|
||||
expect(store.trendData).toEqual(mockTrendData)
|
||||
expect(store.breakdownData).toEqual(mockBreakdownData)
|
||||
})
|
||||
|
||||
it('S11: one API failure does not block others', async () => {
|
||||
const { api } = await import('@/utils/request')
|
||||
vi.mocked(api.get).mockImplementation((url: string) => {
|
||||
if (url.includes('overview')) return Promise.reject(new Error('概览失败')) as never
|
||||
if (url.includes('trend')) return Promise.resolve(mockTrendData) as never
|
||||
if (url.includes('breakdown')) return Promise.resolve(mockBreakdownData) as never
|
||||
return Promise.resolve(null) as never
|
||||
})
|
||||
|
||||
const store = useDashboardStore()
|
||||
await store.fetchAll()
|
||||
|
||||
expect(store.overview).toBeNull()
|
||||
expect(store.overviewError).toBe('概览失败')
|
||||
expect(store.trendData).toEqual(mockTrendData)
|
||||
expect(store.breakdownData).toEqual(mockBreakdownData)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,125 @@
|
||||
import { api } from '@/utils/request'
|
||||
import type {
|
||||
DashboardOverview,
|
||||
DashboardTrendPoint,
|
||||
DashboardTrendParams,
|
||||
DashboardBreakdownItem,
|
||||
DashboardBreakdownParams,
|
||||
} from '@/types/api'
|
||||
|
||||
export const useDashboardStore = defineStore('dashboard', () => {
|
||||
// ─── Overview ───
|
||||
const overview = ref<DashboardOverview | null>(null)
|
||||
const overviewLoading = ref(false)
|
||||
const overviewError = ref('')
|
||||
|
||||
// ─── Trend ───
|
||||
const trendData = ref<DashboardTrendPoint[]>([])
|
||||
const trendLoading = ref(false)
|
||||
const trendError = ref('')
|
||||
const trendGroupBy = ref<'day' | 'week' | 'month'>('day')
|
||||
const trendDataType = ref<string | undefined>(undefined)
|
||||
const trendFrom = ref<string | undefined>(undefined)
|
||||
const trendTo = ref<string | undefined>(undefined)
|
||||
|
||||
// ─── Breakdown ───
|
||||
const breakdownData = ref<DashboardBreakdownItem[]>([])
|
||||
const breakdownLoading = ref(false)
|
||||
const breakdownError = ref('')
|
||||
const breakdownDimension = ref<'company' | 'platform' | 'store'>('company')
|
||||
const breakdownDataType = ref<string | undefined>(undefined)
|
||||
const breakdownFrom = ref<string | undefined>(undefined)
|
||||
const breakdownTo = ref<string | undefined>(undefined)
|
||||
|
||||
async function fetchOverview() {
|
||||
overviewLoading.value = true
|
||||
overviewError.value = ''
|
||||
try {
|
||||
overview.value = await api.get<DashboardOverview>('/api/v1/dashboard/overview')
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : '获取概览数据失败'
|
||||
overviewError.value = msg
|
||||
message.error(msg)
|
||||
} finally {
|
||||
overviewLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchTrend() {
|
||||
trendLoading.value = true
|
||||
trendError.value = ''
|
||||
try {
|
||||
const params: DashboardTrendParams = {
|
||||
group_by: trendGroupBy.value,
|
||||
data_type: trendDataType.value,
|
||||
from: trendFrom.value,
|
||||
to: trendTo.value,
|
||||
}
|
||||
trendData.value = await api.get<DashboardTrendPoint[]>(
|
||||
'/api/v1/dashboard/trend',
|
||||
params as unknown as Record<string, unknown>,
|
||||
)
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : '获取趋势数据失败'
|
||||
trendError.value = msg
|
||||
message.error(msg)
|
||||
} finally {
|
||||
trendLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchBreakdown() {
|
||||
breakdownLoading.value = true
|
||||
breakdownError.value = ''
|
||||
try {
|
||||
const params: DashboardBreakdownParams = {
|
||||
dimension: breakdownDimension.value,
|
||||
data_type: breakdownDataType.value,
|
||||
from: breakdownFrom.value,
|
||||
to: breakdownTo.value,
|
||||
}
|
||||
breakdownData.value = await api.get<DashboardBreakdownItem[]>(
|
||||
'/api/v1/dashboard/breakdown',
|
||||
params as unknown as Record<string, unknown>,
|
||||
)
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : '获取分维度数据失败'
|
||||
breakdownError.value = msg
|
||||
message.error(msg)
|
||||
} finally {
|
||||
breakdownLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAll() {
|
||||
await Promise.allSettled([fetchOverview(), fetchTrend(), fetchBreakdown()])
|
||||
}
|
||||
|
||||
return {
|
||||
// Overview
|
||||
overview,
|
||||
overviewLoading,
|
||||
overviewError,
|
||||
// Trend
|
||||
trendData,
|
||||
trendLoading,
|
||||
trendError,
|
||||
trendGroupBy,
|
||||
trendDataType,
|
||||
trendFrom,
|
||||
trendTo,
|
||||
// Breakdown
|
||||
breakdownData,
|
||||
breakdownLoading,
|
||||
breakdownError,
|
||||
breakdownDimension,
|
||||
breakdownDataType,
|
||||
breakdownFrom,
|
||||
breakdownTo,
|
||||
// Actions
|
||||
fetchOverview,
|
||||
fetchTrend,
|
||||
fetchBreakdown,
|
||||
fetchAll,
|
||||
}
|
||||
})
|
||||
@@ -208,6 +208,51 @@ export interface FailedMessageFilters {
|
||||
failed_at_range: [string, string] | null
|
||||
}
|
||||
|
||||
/** ─── Dashboard 统计 ─── */
|
||||
|
||||
export interface SuccessFailedCount {
|
||||
success: number
|
||||
failed: number
|
||||
}
|
||||
|
||||
export interface DataTypeCount extends SuccessFailedCount {
|
||||
data_type: string
|
||||
}
|
||||
|
||||
export interface DashboardOverview {
|
||||
today: SuccessFailedCount
|
||||
this_week: SuccessFailedCount
|
||||
this_month: SuccessFailedCount
|
||||
by_type: DataTypeCount[]
|
||||
}
|
||||
|
||||
export interface DashboardTrendPoint {
|
||||
date: string
|
||||
success: number
|
||||
failed: number
|
||||
}
|
||||
|
||||
export interface DashboardTrendParams {
|
||||
from?: string
|
||||
to?: string
|
||||
group_by?: 'day' | 'week' | 'month'
|
||||
data_type?: string
|
||||
}
|
||||
|
||||
export interface DashboardBreakdownItem {
|
||||
id: number
|
||||
name: string
|
||||
success: number
|
||||
failed: number
|
||||
}
|
||||
|
||||
export interface DashboardBreakdownParams {
|
||||
dimension?: 'company' | 'platform' | 'store'
|
||||
from?: string
|
||||
to?: string
|
||||
data_type?: string
|
||||
}
|
||||
|
||||
/** 业务异常 */
|
||||
export class ApiError extends Error {
|
||||
code: number
|
||||
|
||||
Reference in New Issue
Block a user