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
|
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 {
|
export class ApiError extends Error {
|
||||||
code: number
|
code: number
|
||||||
|
|||||||
Reference in New Issue
Block a user