feat(admin-web): 实现 Dashboard 页面真实 API 接入
## 概述 将 admin-web Dashboard 页面从模拟数据改为真实 API 调用, 使用 React Query 实现数据获取、缓存和自动刷新。 ## 新增文件 - dashboardService.ts: Dashboard API 服务封装 - useDashboard.ts: React Query hooks - dashboard.types.ts: Dashboard 类型定义 ## API 接入 - /dashboard/stats: 统计卡片(总认种量、总用户数、省/市公司数) - /dashboard/charts: 趋势图表(支持 7d/30d/90d 周期切换) - /dashboard/region: 区域分布 - /dashboard/activities: 最近活动 ## UI 优化 - 添加加载骨架屏 - 添加错误重试机制 - 添加空数据提示 - 优化图表周期切换交互 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
0e367d042c
commit
900db13e8d
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = () => (
|
||||
<div className={styles.dashboard__skeleton}>
|
||||
<div className={styles.dashboard__skeletonTitle} />
|
||||
<div className={styles.dashboard__skeletonValue} />
|
||||
<div className={styles.dashboard__skeletonChange} />
|
||||
</div>
|
||||
);
|
||||
|
||||
// 模拟趋势数据
|
||||
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 }) => (
|
||||
<div className={styles.dashboard__error}>
|
||||
<span>{message}</span>
|
||||
{onRetry && (
|
||||
<Button variant="outline" size="sm" onClick={onRetry}>
|
||||
重试
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
// 模拟区域分布数据
|
||||
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 }) => (
|
||||
<div className={styles.dashboard__empty}>
|
||||
<span>{message}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default function DashboardPage() {
|
||||
const [trendPeriod, setTrendPeriod] = useState<DashboardPeriod>('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 = (
|
||||
<>
|
||||
<Button variant="outline" size="sm">
|
||||
|
|
@ -128,25 +99,79 @@ export default function DashboardPage() {
|
|||
<div className={styles.dashboard}>
|
||||
{/* 统计卡片区 */}
|
||||
<div className={styles.dashboard__stats}>
|
||||
{statsData.map((stat, index) => (
|
||||
<StatCard key={index} {...stat} />
|
||||
))}
|
||||
{statsLoading ? (
|
||||
// 加载状态显示骨架屏
|
||||
<>
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<StatCardSkeleton key={i} />
|
||||
))}
|
||||
</>
|
||||
) : statsError ? (
|
||||
// 错误状态
|
||||
<div className={styles.dashboard__statsError}>
|
||||
<ErrorMessage
|
||||
message="加载统计数据失败"
|
||||
onRetry={() => refetchStats()}
|
||||
/>
|
||||
</div>
|
||||
) : statsData && statsData.length > 0 ? (
|
||||
// 正常显示数据
|
||||
statsData.map((stat, index) => (
|
||||
<StatCard key={index} {...stat} />
|
||||
))
|
||||
) : (
|
||||
// 空数据状态
|
||||
<EmptyData message="暂无统计数据" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 图表区 */}
|
||||
<div className={styles.dashboard__charts}>
|
||||
<div className={styles.dashboard__mainChart}>
|
||||
<TrendChart title="认种趋势" data={trendData} />
|
||||
{trendError ? (
|
||||
<ErrorMessage
|
||||
message="加载趋势数据失败"
|
||||
onRetry={() => refetchTrend()}
|
||||
/>
|
||||
) : (
|
||||
<TrendChart
|
||||
title="认种趋势"
|
||||
data={trendData?.data ?? []}
|
||||
period={trendPeriod}
|
||||
onPeriodChange={setTrendPeriod}
|
||||
loading={trendLoading}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.dashboard__sidePanel}>
|
||||
<RecentActivity activities={activityData} />
|
||||
{activitiesLoading ? (
|
||||
<div className={styles.dashboard__activitiesLoading}>加载中...</div>
|
||||
) : activitiesError ? (
|
||||
<ErrorMessage
|
||||
message="加载活动数据失败"
|
||||
onRetry={() => refetchActivities()}
|
||||
/>
|
||||
) : (
|
||||
<RecentActivity activities={activitiesData ?? []} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 底部区域 */}
|
||||
<div className={styles.dashboard__bottom}>
|
||||
<div className={styles.dashboard__regionChart}>
|
||||
<RegionDistribution data={regionData} />
|
||||
{regionLoading ? (
|
||||
<div className={styles.dashboard__regionLoading}>加载中...</div>
|
||||
) : regionError ? (
|
||||
<ErrorMessage
|
||||
message="加载区域分布数据失败"
|
||||
onRetry={() => refetchRegion()}
|
||||
/>
|
||||
) : regionData && regionData.length > 0 ? (
|
||||
<RegionDistribution data={regionData} />
|
||||
) : (
|
||||
<EmptyData message="暂无区域分布数据" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,4 +13,13 @@
|
|||
width: 100%;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
&__loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
'use client';
|
||||
|
||||
import { FC, useState } from 'react';
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Area, AreaChart } from 'recharts';
|
||||
import { XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Area, AreaChart } from 'recharts';
|
||||
import { Card, Button } from '@/components/common';
|
||||
import type { DashboardPeriod } from '@/types';
|
||||
import styles from './TrendChart.module.scss';
|
||||
|
||||
interface TrendData {
|
||||
|
|
@ -13,16 +14,35 @@ interface TrendData {
|
|||
export interface TrendChartProps {
|
||||
title: string;
|
||||
data: TrendData[];
|
||||
period?: DashboardPeriod;
|
||||
onPeriodChange?: (period: DashboardPeriod) => void;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const timeRanges = [
|
||||
const timeRanges: { label: string; value: DashboardPeriod }[] = [
|
||||
{ label: '7天', value: '7d' },
|
||||
{ label: '30天', value: '30d' },
|
||||
{ label: '90天', value: '90d' },
|
||||
];
|
||||
|
||||
export const TrendChart: FC<TrendChartProps> = ({ title, data }) => {
|
||||
const [activeRange, setActiveRange] = useState('7d');
|
||||
export const TrendChart: FC<TrendChartProps> = ({
|
||||
title,
|
||||
data,
|
||||
period: externalPeriod,
|
||||
onPeriodChange,
|
||||
loading = false,
|
||||
}) => {
|
||||
const [internalPeriod, setInternalPeriod] = useState<DashboardPeriod>('7d');
|
||||
|
||||
// 支持受控和非受控两种模式
|
||||
const activePeriod = externalPeriod ?? internalPeriod;
|
||||
const handlePeriodChange = (newPeriod: DashboardPeriod) => {
|
||||
if (onPeriodChange) {
|
||||
onPeriodChange(newPeriod);
|
||||
} else {
|
||||
setInternalPeriod(newPeriod);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
|
|
@ -33,8 +53,8 @@ export const TrendChart: FC<TrendChartProps> = ({ title, data }) => {
|
|||
<Button
|
||||
key={range.value}
|
||||
size="sm"
|
||||
variant={activeRange === range.value ? 'primary' : 'ghost'}
|
||||
onClick={() => setActiveRange(range.value)}
|
||||
variant={activePeriod === range.value ? 'primary' : 'ghost'}
|
||||
onClick={() => handlePeriodChange(range.value)}
|
||||
>
|
||||
{range.label}
|
||||
</Button>
|
||||
|
|
@ -44,46 +64,52 @@ export const TrendChart: FC<TrendChartProps> = ({ title, data }) => {
|
|||
className={styles.trendChart}
|
||||
>
|
||||
<div className={styles.trendChart__chart}>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<AreaChart data={data} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}>
|
||||
<defs>
|
||||
<linearGradient id="colorValue" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#1565C0" stopOpacity={0.2} />
|
||||
<stop offset="95%" stopColor="#1565C0" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#E0E0E0" vertical={false} />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fontSize: 12, fill: '#757575' }}
|
||||
/>
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fontSize: 12, fill: '#757575' }}
|
||||
width={40}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #E0E0E0',
|
||||
borderRadius: 8,
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
|
||||
}}
|
||||
labelStyle={{ color: '#212121', fontWeight: 500, marginBottom: 4 }}
|
||||
itemStyle={{ color: '#1565C0' }}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke="#1565C0"
|
||||
strokeWidth={2}
|
||||
fill="url(#colorValue)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
{loading ? (
|
||||
<div className={styles.trendChart__loading}>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
) : (
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<AreaChart data={data} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}>
|
||||
<defs>
|
||||
<linearGradient id="colorValue" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#1565C0" stopOpacity={0.2} />
|
||||
<stop offset="95%" stopColor="#1565C0" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#E0E0E0" vertical={false} />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fontSize: 12, fill: '#757575' }}
|
||||
/>
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fontSize: 12, fill: '#757575' }}
|
||||
width={40}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #E0E0E0',
|
||||
borderRadius: 8,
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
|
||||
}}
|
||||
labelStyle={{ color: '#212121', fontWeight: 500, marginBottom: 4 }}
|
||||
itemStyle={{ color: '#1565C0' }}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke="#1565C0"
|
||||
strokeWidth={2}
|
||||
fill="url(#colorValue)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
// Hooks 统一导出
|
||||
|
||||
export * from './useDashboard';
|
||||
|
|
@ -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, // 每分钟自动刷新
|
||||
});
|
||||
}
|
||||
|
|
@ -84,5 +84,6 @@ export const API_ENDPOINTS = {
|
|||
STATS: '/dashboard/stats',
|
||||
ACTIVITIES: '/dashboard/activities',
|
||||
CHARTS: '/dashboard/charts',
|
||||
REGION: '/dashboard/region',
|
||||
},
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -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<ApiResponse<DashboardOverviewResponse>> {
|
||||
return apiClient.get(API_ENDPOINTS.DASHBOARD.OVERVIEW);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取仪表板统计卡片数据
|
||||
*/
|
||||
async getStats(): Promise<ApiResponse<{ stats: DashboardStatItem[] }>> {
|
||||
return apiClient.get(API_ENDPOINTS.DASHBOARD.STATS);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取趋势图表数据
|
||||
* @param period 时间周期 (7d | 30d | 90d)
|
||||
*/
|
||||
async getTrendData(period: DashboardPeriod = '7d'): Promise<ApiResponse<DashboardTrendResponse>> {
|
||||
return apiClient.get(API_ENDPOINTS.DASHBOARD.CHARTS, {
|
||||
params: { period },
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取区域分布数据
|
||||
*/
|
||||
async getRegionDistribution(): Promise<ApiResponse<DashboardRegionResponse>> {
|
||||
return apiClient.get(API_ENDPOINTS.DASHBOARD.REGION);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取最近活动列表
|
||||
* @param limit 返回数量限制
|
||||
*/
|
||||
async getRecentActivities(limit = 5): Promise<ApiResponse<DashboardActivitiesResponse>> {
|
||||
return apiClient.get(API_ENDPOINTS.DASHBOARD.ACTIVITIES, {
|
||||
params: { limit },
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default dashboardService;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -5,3 +5,4 @@ export * from './user.types';
|
|||
export * from './company.types';
|
||||
export * from './statistics.types';
|
||||
export * from './common.types';
|
||||
export * from './dashboard.types';
|
||||
|
|
|
|||
Loading…
Reference in New Issue