diff --git a/frontend/admin-web/src/app/(dashboard)/dashboard/dashboard.module.scss b/frontend/admin-web/src/app/(dashboard)/dashboard/dashboard.module.scss index 322e91d8..ce93a4f8 100644 --- a/frontend/admin-web/src/app/(dashboard)/dashboard/dashboard.module.scss +++ b/frontend/admin-web/src/app/(dashboard)/dashboard/dashboard.module.scss @@ -20,6 +20,10 @@ } } + &__statsError { + grid-column: 1 / -1; + } + &__charts { display: grid; grid-template-columns: 1fr 360px; @@ -47,4 +51,89 @@ &__regionChart { min-height: 300px; } + + // 骨架屏样式 + &__skeleton { + background: $color-bg-secondary; + border-radius: $border-radius-lg; + padding: $spacing-lg; + display: flex; + flex-direction: column; + gap: $spacing-md; + } + + &__skeletonTitle { + width: 60%; + height: 16px; + background: linear-gradient(90deg, $color-border 25%, $color-bg-tertiary 50%, $color-border 75%); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; + border-radius: $border-radius-sm; + } + + &__skeletonValue { + width: 80%; + height: 32px; + background: linear-gradient(90deg, $color-border 25%, $color-bg-tertiary 50%, $color-border 75%); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; + border-radius: $border-radius-sm; + } + + &__skeletonChange { + width: 40%; + height: 14px; + background: linear-gradient(90deg, $color-border 25%, $color-bg-tertiary 50%, $color-border 75%); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; + border-radius: $border-radius-sm; + } + + // 错误提示样式 + &__error { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: $spacing-md; + padding: $spacing-xl; + background: $color-bg-secondary; + border-radius: $border-radius-lg; + color: $color-text-secondary; + min-height: 120px; + } + + // 空数据提示样式 + &__empty { + display: flex; + align-items: center; + justify-content: center; + padding: $spacing-xl; + background: $color-bg-secondary; + border-radius: $border-radius-lg; + color: $color-text-tertiary; + grid-column: 1 / -1; + min-height: 120px; + } + + // 加载中提示 + &__activitiesLoading, + &__regionLoading { + display: flex; + align-items: center; + justify-content: center; + background: $color-bg-secondary; + border-radius: $border-radius-lg; + color: $color-text-secondary; + min-height: 100%; + } +} + +@keyframes shimmer { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } } diff --git a/frontend/admin-web/src/app/(dashboard)/dashboard/page.tsx b/frontend/admin-web/src/app/(dashboard)/dashboard/page.tsx index 28c1a5ec..ffef9a2d 100644 --- a/frontend/admin-web/src/app/(dashboard)/dashboard/page.tsx +++ b/frontend/admin-web/src/app/(dashboard)/dashboard/page.tsx @@ -1,110 +1,81 @@ 'use client'; +import { useState } from 'react'; import { Button } from '@/components/common'; import { PageContainer } from '@/components/layout'; import { StatCard } from '@/components/features/dashboard/StatCard'; import { TrendChart } from '@/components/features/dashboard/TrendChart'; import { RegionDistribution } from '@/components/features/dashboard/RegionDistribution'; import { RecentActivity } from '@/components/features/dashboard/RecentActivity'; +import { + useDashboardStats, + useDashboardTrend, + useDashboardRegion, + useDashboardActivities, +} from '@/hooks'; +import type { DashboardPeriod } from '@/types'; import styles from './dashboard.module.scss'; -// 模拟统计数据 -const statsData = [ - { - title: '总认种量', - value: 12580, - suffix: '棵', - change: { value: 5.6, trend: 'up' as const }, - color: '#1565C0', - }, - { - title: '活跃用户', - value: 3240, - suffix: '人', - change: { value: 3.2, trend: 'up' as const }, - color: '#4CAF50', - }, - { - title: '省级公司', - value: 28, - suffix: '家', - change: { value: 2.1, trend: 'up' as const }, - color: '#F5A623', - }, - { - title: '市级公司', - value: 156, - suffix: '家', - change: { value: 4.8, trend: 'up' as const }, - color: '#9C27B0', - }, -]; +// 骨架屏组件 +const StatCardSkeleton = () => ( +
+
+
+
+
+); -// 模拟趋势数据 -const trendData = [ - { date: '03-19', value: 150 }, - { date: '03-20', value: 280 }, - { date: '03-21', value: 320 }, - { date: '03-22', value: 250 }, - { date: '03-23', value: 380 }, - { date: '03-24', value: 420 }, - { date: '03-25', value: 390 }, -]; +// 错误提示组件 +const ErrorMessage = ({ message, onRetry }: { message: string; onRetry?: () => void }) => ( +
+ {message} + {onRetry && ( + + )} +
+); -// 模拟区域分布数据 -const regionData = [ - { region: '华东地区', percentage: 35, color: '#1565C0' }, - { region: '华南地区', percentage: 25, color: '#4CAF50' }, - { region: '华北地区', percentage: 20, color: '#F5A623' }, - { region: '华中地区', percentage: 12, color: '#9C27B0' }, - { region: '其他地区', percentage: 8, color: '#607D8B' }, -]; - -// 模拟最近活动数据 -const activityData = [ - { - id: '1', - type: 'user_register' as const, - icon: '👤', - title: '新用户注册', - description: '用户 张三 完成注册', - timestamp: '5分钟前', - }, - { - id: '2', - type: 'company_activity' as const, - icon: '🏢', - title: '公司授权', - description: '广东省公司完成授权', - timestamp: '15分钟前', - }, - { - id: '3', - type: 'system_update' as const, - icon: '⚙️', - title: '系统更新', - description: '龙虎榜规则已更新', - timestamp: '1小时前', - }, - { - id: '4', - type: 'report_generated' as const, - icon: '📊', - title: '报表生成', - description: '3月份运营报表已生成', - timestamp: '2小时前', - }, - { - id: '5', - type: 'user_register' as const, - icon: '👤', - title: '新用户注册', - description: '用户 李四 完成注册', - timestamp: '3小时前', - }, -]; +// 空数据提示 +const EmptyData = ({ message }: { message: string }) => ( +
+ {message} +
+); export default function DashboardPage() { + const [trendPeriod, setTrendPeriod] = useState('7d'); + + // 使用 React Query hooks 获取数据 + const { + data: statsData, + isLoading: statsLoading, + error: statsError, + refetch: refetchStats, + } = useDashboardStats(); + + const { + data: trendData, + isLoading: trendLoading, + error: trendError, + refetch: refetchTrend, + } = useDashboardTrend(trendPeriod); + + const { + data: regionData, + isLoading: regionLoading, + error: regionError, + refetch: refetchRegion, + } = useDashboardRegion(); + + const { + data: activitiesData, + isLoading: activitiesLoading, + error: activitiesError, + refetch: refetchActivities, + } = useDashboardActivities(5); + const headerActions = ( <> @@ -44,46 +64,52 @@ export const TrendChart: FC = ({ title, data }) => { className={styles.trendChart} >
- - - - - - - - - - - - - - - + {loading ? ( +
+ 加载中... +
+ ) : ( + + + + + + + + + + + + + + + + )}
); diff --git a/frontend/admin-web/src/hooks/index.ts b/frontend/admin-web/src/hooks/index.ts new file mode 100644 index 00000000..b8353586 --- /dev/null +++ b/frontend/admin-web/src/hooks/index.ts @@ -0,0 +1,3 @@ +// Hooks 统一导出 + +export * from './useDashboard'; diff --git a/frontend/admin-web/src/hooks/useDashboard.ts b/frontend/admin-web/src/hooks/useDashboard.ts new file mode 100644 index 00000000..4408cc71 --- /dev/null +++ b/frontend/admin-web/src/hooks/useDashboard.ts @@ -0,0 +1,96 @@ +/** + * 仪表板数据 Hooks + * 使用 React Query 进行数据获取和缓存管理 + */ + +import { useQuery } from '@tanstack/react-query'; +import { dashboardService } from '@/services/dashboardService'; +import type { DashboardPeriod } from '@/types'; + +/** Query Keys */ +export const dashboardKeys = { + all: ['dashboard'] as const, + overview: () => [...dashboardKeys.all, 'overview'] as const, + stats: () => [...dashboardKeys.all, 'stats'] as const, + trend: (period: DashboardPeriod) => [...dashboardKeys.all, 'trend', period] as const, + region: () => [...dashboardKeys.all, 'region'] as const, + activities: (limit: number) => [...dashboardKeys.all, 'activities', limit] as const, +}; + +/** + * 获取仪表板概览数据 + */ +export function useDashboardOverview() { + return useQuery({ + queryKey: dashboardKeys.overview(), + queryFn: async () => { + const response = await dashboardService.getOverview(); + return response.data; + }, + staleTime: 30 * 1000, // 30秒后标记为过期 + gcTime: 5 * 60 * 1000, // 5分钟后垃圾回收 + }); +} + +/** + * 获取统计卡片数据 + */ +export function useDashboardStats() { + return useQuery({ + queryKey: dashboardKeys.stats(), + queryFn: async () => { + const response = await dashboardService.getStats(); + return response.data.stats; + }, + staleTime: 30 * 1000, + gcTime: 5 * 60 * 1000, + }); +} + +/** + * 获取趋势图表数据 + * @param period 时间周期 + */ +export function useDashboardTrend(period: DashboardPeriod = '7d') { + return useQuery({ + queryKey: dashboardKeys.trend(period), + queryFn: async () => { + const response = await dashboardService.getTrendData(period); + return response.data.trend; + }, + staleTime: 60 * 1000, // 1分钟后标记为过期 + gcTime: 5 * 60 * 1000, + }); +} + +/** + * 获取区域分布数据 + */ +export function useDashboardRegion() { + return useQuery({ + queryKey: dashboardKeys.region(), + queryFn: async () => { + const response = await dashboardService.getRegionDistribution(); + return response.data.regions; + }, + staleTime: 5 * 60 * 1000, // 5分钟后标记为过期 + gcTime: 10 * 60 * 1000, + }); +} + +/** + * 获取最近活动数据 + * @param limit 返回数量限制 + */ +export function useDashboardActivities(limit = 5) { + return useQuery({ + queryKey: dashboardKeys.activities(limit), + queryFn: async () => { + const response = await dashboardService.getRecentActivities(limit); + return response.data.activities; + }, + staleTime: 30 * 1000, // 30秒后标记为过期 + gcTime: 5 * 60 * 1000, + refetchInterval: 60 * 1000, // 每分钟自动刷新 + }); +} diff --git a/frontend/admin-web/src/infrastructure/api/endpoints.ts b/frontend/admin-web/src/infrastructure/api/endpoints.ts index ea20634f..1e0db771 100644 --- a/frontend/admin-web/src/infrastructure/api/endpoints.ts +++ b/frontend/admin-web/src/infrastructure/api/endpoints.ts @@ -84,5 +84,6 @@ export const API_ENDPOINTS = { STATS: '/dashboard/stats', ACTIVITIES: '/dashboard/activities', CHARTS: '/dashboard/charts', + REGION: '/dashboard/region', }, } as const; diff --git a/frontend/admin-web/src/services/dashboardService.ts b/frontend/admin-web/src/services/dashboardService.ts new file mode 100644 index 00000000..2b031d7d --- /dev/null +++ b/frontend/admin-web/src/services/dashboardService.ts @@ -0,0 +1,85 @@ +/** + * 仪表板服务 + * 负责仪表板数据的API调用 + */ + +import apiClient from '@/infrastructure/api/client'; +import { API_ENDPOINTS } from '@/infrastructure/api/endpoints'; +import type { + ApiResponse, + DashboardOverview, + DashboardTrendData, + DashboardPeriod, + RegionDistributionItem, + DashboardActivity, + DashboardStatItem, +} from '@/types'; + +/** 仪表板概览响应 */ +interface DashboardOverviewResponse { + stats: DashboardStatItem[]; + overview: DashboardOverview; +} + +/** 仪表板趋势响应 */ +interface DashboardTrendResponse { + trend: DashboardTrendData; +} + +/** 仪表板区域分布响应 */ +interface DashboardRegionResponse { + regions: RegionDistributionItem[]; +} + +/** 仪表板活动响应 */ +interface DashboardActivitiesResponse { + activities: DashboardActivity[]; +} + +/** + * 仪表板服务 + */ +export const dashboardService = { + /** + * 获取仪表板概览数据(统计卡片) + */ + async getOverview(): Promise> { + return apiClient.get(API_ENDPOINTS.DASHBOARD.OVERVIEW); + }, + + /** + * 获取仪表板统计卡片数据 + */ + async getStats(): Promise> { + return apiClient.get(API_ENDPOINTS.DASHBOARD.STATS); + }, + + /** + * 获取趋势图表数据 + * @param period 时间周期 (7d | 30d | 90d) + */ + async getTrendData(period: DashboardPeriod = '7d'): Promise> { + return apiClient.get(API_ENDPOINTS.DASHBOARD.CHARTS, { + params: { period }, + }); + }, + + /** + * 获取区域分布数据 + */ + async getRegionDistribution(): Promise> { + return apiClient.get(API_ENDPOINTS.DASHBOARD.REGION); + }, + + /** + * 获取最近活动列表 + * @param limit 返回数量限制 + */ + async getRecentActivities(limit = 5): Promise> { + return apiClient.get(API_ENDPOINTS.DASHBOARD.ACTIVITIES, { + params: { limit }, + }); + }, +}; + +export default dashboardService; diff --git a/frontend/admin-web/src/types/dashboard.types.ts b/frontend/admin-web/src/types/dashboard.types.ts new file mode 100644 index 00000000..19c7d8fe --- /dev/null +++ b/frontend/admin-web/src/types/dashboard.types.ts @@ -0,0 +1,89 @@ +// 仪表板类型定义 + +/** 趋势变化方向 */ +export type TrendDirection = 'up' | 'down'; + +/** 时间周期 */ +export type DashboardPeriod = '7d' | '30d' | '90d'; + +/** 活动类型 */ +export type ActivityType = + | 'user_register' + | 'company_authorization' + | 'planting_order' + | 'system_update' + | 'report_generated'; + +/** 统计卡片变化数据 */ +export interface StatChange { + value: number; + trend: TrendDirection; +} + +/** 统计卡片数据 */ +export interface DashboardStatItem { + title: string; + value: number; + suffix: string; + change: StatChange; + color: string; +} + +/** 仪表板概览数据 */ +export interface DashboardOverview { + totalPlantingCount: number; + totalPlantingChange: StatChange; + activeUserCount: number; + activeUserChange: StatChange; + provinceCompanyCount: number; + provinceCompanyChange: StatChange; + cityCompanyCount: number; + cityCompanyChange: StatChange; +} + +/** 趋势数据点 */ +export interface TrendDataPoint { + date: string; + value: number; +} + +/** 趋势图表数据 */ +export interface DashboardTrendData { + period: DashboardPeriod; + data: TrendDataPoint[]; +} + +/** 区域分布数据 */ +export interface RegionDistributionItem { + region: string; + percentage: number; + color: string; +} + +/** 活动记录 */ +export interface DashboardActivity { + id: string; + type: ActivityType; + icon: string; + title: string; + description: string; + timestamp: string; + createdAt: string; +} + +/** 仪表板完整数据 */ +export interface DashboardData { + overview: DashboardOverview; + trend: DashboardTrendData; + regionDistribution: RegionDistributionItem[]; + recentActivities: DashboardActivity[]; +} + +/** 仪表板API请求参数 */ +export interface DashboardTrendParams { + period: DashboardPeriod; +} + +export interface DashboardActivitiesParams { + limit?: number; +} diff --git a/frontend/admin-web/src/types/index.ts b/frontend/admin-web/src/types/index.ts index 42f133a8..4941f471 100644 --- a/frontend/admin-web/src/types/index.ts +++ b/frontend/admin-web/src/types/index.ts @@ -5,3 +5,4 @@ export * from './user.types'; export * from './company.types'; export * from './statistics.types'; export * from './common.types'; +export * from './dashboard.types';