rwadurian/frontend/admin-web/src/app/(dashboard)/statistics/page.tsx

300 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 数据统计页面
* [2026-01-04] 更新新增系统账户报表Tab
* [2026-01-06] 更新:认种统计改为真实数据,实现趋势图表
* 回滚方式:删除 SystemAccountsTab 导入及相关 mainTab state 和切换逻辑
*/
'use client';
import { useState, useEffect } from 'react';
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Legend,
} from 'recharts';
import { PageContainer } from '@/components/layout';
import { cn } from '@/utils/helpers';
// [2026-01-04] 新增系统账户报表Tab
import { SystemAccountsTab } from '@/components/features/system-account-report';
import { dashboardService } from '@/services/dashboardService';
import type { PlantingGlobalStats, PlantingTrendPeriod, PlantingTrendDataPoint } from '@/types';
import styles from './statistics.module.scss';
/**
* 格式化数字显示
*/
function formatNumber(num: number): string {
return num.toLocaleString('zh-CN');
}
/**
* 格式化金额显示(积分)
*/
function formatAmount(amount: string): string {
const num = parseFloat(amount);
if (isNaN(num)) return '0';
return num.toLocaleString('zh-CN', { maximumFractionDigits: 2 });
}
/**
* 格式化趋势图标签
*/
function formatTrendLabel(label: string, period: PlantingTrendPeriod): string {
if (!label) return '';
switch (period) {
case 'day':
// 2026-01-06 -> 01-06
return label.slice(5);
case 'week':
// 2026-W01 -> W01
return label.slice(5);
case 'month':
// 2026-01 -> 01月
return label.slice(5) + '月';
case 'quarter':
// 2026-Q1 -> Q1
return label.slice(5);
case 'year':
// 2026 -> 2026年
return label + '年';
default:
return label;
}
}
/**
* 数据统计页面
* [2026-01-04] 更新新增系统账户报表Tab
* [2026-01-06] 更新:认种统计改为真实数据,实现趋势图表
*/
export default function StatisticsPage() {
// [2026-01-04] 新增主Tab切换 - 数据统计 vs 系统账户
const [mainTab, setMainTab] = useState<'statistics' | 'system-accounts'>('statistics');
// 趋势图时间维度
const [trendPeriod, setTrendPeriod] = useState<PlantingTrendPeriod>('day');
// [2026-01-06] 新增:认种统计数据状态
const [plantingStats, setPlantingStats] = useState<PlantingGlobalStats | null>(null);
const [statsLoading, setStatsLoading] = useState(true);
const [statsError, setStatsError] = useState<string | null>(null);
// [2026-01-06] 新增:趋势数据状态
const [trendData, setTrendData] = useState<PlantingTrendDataPoint[]>([]);
const [trendLoading, setTrendLoading] = useState(true);
const [trendError, setTrendError] = useState<string | null>(null);
// [2026-01-06] 新增:获取认种统计数据
useEffect(() => {
async function fetchPlantingStats() {
try {
setStatsLoading(true);
setStatsError(null);
const response = await dashboardService.getPlantingStats();
if (response.code === 0 && response.data) {
setPlantingStats(response.data);
} else {
setStatsError(response.message || '获取统计数据失败');
}
} catch (error) {
console.error('获取认种统计失败:', error);
setStatsError('获取统计数据失败');
} finally {
setStatsLoading(false);
}
}
fetchPlantingStats();
}, []);
// [2026-01-06] 新增:获取趋势数据
useEffect(() => {
async function fetchTrendData() {
try {
setTrendLoading(true);
setTrendError(null);
const response = await dashboardService.getPlantingTrendData(trendPeriod);
if (response.code === 0 && response.data) {
setTrendData(response.data);
} else {
setTrendError(response.message || '获取趋势数据失败');
}
} catch (error) {
console.error('获取趋势数据失败:', error);
setTrendError('获取趋势数据失败');
} finally {
setTrendLoading(false);
}
}
fetchTrendData();
}, [trendPeriod]);
// 处理趋势数据用于图表显示
const chartData = trendData.map(item => ({
name: formatTrendLabel(item.label, trendPeriod),
认种棵数: item.treeCount,
订单数: item.orderCount,
}));
return (
<PageContainer title="数据统计">
<div className={styles.statistics}>
{/* 页面标题 */}
<h2 className={styles.statistics__title}></h2>
{/* [2026-01-04] 新增主Tab切换 */}
<div className={styles.statistics__mainTabs}>
<button
className={cn(styles.statistics__mainTab, mainTab === 'statistics' && styles['statistics__mainTab--active'])}
onClick={() => setMainTab('statistics')}
>
</button>
<button
className={cn(styles.statistics__mainTab, mainTab === 'system-accounts' && styles['statistics__mainTab--active'])}
onClick={() => setMainTab('system-accounts')}
>
</button>
</div>
{/* [2026-01-04] 新增系统账户报表Tab内容 */}
{mainTab === 'system-accounts' && <SystemAccountsTab />}
{/* 认种统计内容 - 仅在 statistics tab 显示 */}
{mainTab === 'statistics' && (
<>
{/* 统计概览卡片 - [2026-01-06] 改为真实数据 */}
<section className={styles.statistics__overview}>
<div className={styles.statistics__overviewCard}>
<div className={styles.statistics__overviewLabel}></div>
{statsLoading ? (
<h1 className={styles.statistics__overviewValue}>...</h1>
) : statsError ? (
<h1 className={styles.statistics__overviewValue}>--</h1>
) : (
<>
<h1 className={styles.statistics__overviewValue}>
{formatNumber(plantingStats?.totalTreeCount ?? 0)}
</h1>
<div className={styles.statistics__overviewSubValue}>
: {formatAmount(plantingStats?.totalAmount ?? '0')}
</div>
</>
)}
</div>
<div className={styles.statistics__overviewCard}>
<div className={styles.statistics__overviewLabel}></div>
{statsLoading ? (
<h1 className={styles.statistics__overviewValue}>...</h1>
) : statsError ? (
<h1 className={styles.statistics__overviewValue}>--</h1>
) : (
<>
<h1 className={styles.statistics__overviewValue}>
{formatNumber(plantingStats?.todayStats?.treeCount ?? 0)}
</h1>
<div className={styles.statistics__overviewSubValue}>
: {formatAmount(plantingStats?.todayStats?.amount ?? '0')}
</div>
</>
)}
</div>
<div className={styles.statistics__overviewCard}>
<div className={styles.statistics__overviewLabel}></div>
{statsLoading ? (
<h1 className={styles.statistics__overviewValue}>...</h1>
) : statsError ? (
<h1 className={styles.statistics__overviewValue}>--</h1>
) : (
<>
<h1 className={styles.statistics__overviewValue}>
{formatNumber(plantingStats?.monthStats?.treeCount ?? 0)}
</h1>
<div className={styles.statistics__overviewSubValue}>
: {formatAmount(plantingStats?.monthStats?.amount ?? '0')}
</div>
</>
)}
</div>
</section>
{/* 榴莲树认种数量趋势 - [2026-01-06] 实现真实图表 */}
<section className={styles.statistics__card}>
<div className={styles.statistics__cardHeader}>
<b className={styles.statistics__cardTitle}></b>
<div className={styles.statistics__periodTabs}>
{(['day', 'week', 'month', 'quarter', 'year'] as const).map((period) => (
<button
key={period}
className={cn(styles.statistics__periodTab, trendPeriod === period && styles['statistics__periodTab--active'])}
onClick={() => setTrendPeriod(period)}
>
{period === 'day' ? '日' : period === 'week' ? '周' : period === 'month' ? '月' : period === 'quarter' ? '季度' : '年度'}
</button>
))}
</div>
</div>
<div className={styles.statistics__chartArea}>
{trendLoading ? (
<div className={styles.statistics__chartLoading}>...</div>
) : trendError ? (
<div className={styles.statistics__chartError}>{trendError}</div>
) : chartData.length === 0 ? (
<div className={styles.statistics__chartEmpty}></div>
) : (
<ResponsiveContainer width="100%" height={300}>
<LineChart data={chartData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
<XAxis
dataKey="name"
tick={{ fontSize: 12, fill: '#6b7280' }}
tickLine={{ stroke: '#e5e7eb' }}
axisLine={{ stroke: '#e5e7eb' }}
/>
<YAxis
tick={{ fontSize: 12, fill: '#6b7280' }}
tickLine={{ stroke: '#e5e7eb' }}
axisLine={{ stroke: '#e5e7eb' }}
allowDecimals={false}
/>
<Tooltip
contentStyle={{
backgroundColor: '#fff',
border: '1px solid #e5e7eb',
borderRadius: '8px',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
}}
/>
<Legend />
<Line
type="monotone"
dataKey="认种棵数"
stroke="#10b981"
strokeWidth={2}
dot={{ fill: '#10b981', strokeWidth: 2, r: 4 }}
activeDot={{ r: 6 }}
/>
<Line
type="monotone"
dataKey="订单数"
stroke="#3b82f6"
strokeWidth={2}
dot={{ fill: '#3b82f6', strokeWidth: 2, r: 4 }}
activeDot={{ r: 6 }}
/>
</LineChart>
</ResponsiveContainer>
)}
</div>
</section>
</>
)}
</div>
</PageContainer>
);
}