import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:intl/intl.dart'; import '../theme/app_theme.dart'; import '../providers/statistics_provider.dart'; import '../models/daily_stats.dart'; import '../widgets/empty_state_widget.dart'; import '../widgets/error_state_widget.dart'; class StatsScreen extends ConsumerStatefulWidget { const StatsScreen({super.key}); @override ConsumerState createState() => _StatsScreenState(); } class _StatsScreenState extends ConsumerState { String _selectedPeriod = '周'; // 日、周、月 // 根据时间段获取图表数据 List> _getChartData(List? stats, String period) { if (stats != null && stats.isNotEmpty) { return stats.map((stat) { return { 'date': stat.date, 'total': stat.totalTime, 'work': stat.workTime, 'study': stat.studyTime, 'entertainment': stat.entertainmentTime, }; }).toList(); } // 如果没有数据,使用默认测试数据 final now = DateTime.now(); if (period == '日') { // 日视图:显示今日数据(24小时,每小时一个点) return List.generate(24, (index) { final hour = now.subtract(Duration(hours: 23 - index)); return { 'date': hour, 'total': 1800 + (index % 3) * 300, // 模拟每小时30-60分钟 'work': 1200 + (index % 3) * 200, 'study': 300 + (index % 3) * 50, 'entertainment': 300 + (index % 3) * 50, }; }); } else if (period == '周') { // 周视图:显示7天数据 return [ {'date': now.subtract(const Duration(days: 6)), 'total': 21600, 'work': 14400, 'study': 3600, 'entertainment': 3600}, {'date': now.subtract(const Duration(days: 5)), 'total': 25200, 'work': 18000, 'study': 3600, 'entertainment': 3600}, {'date': now.subtract(const Duration(days: 4)), 'total': 23400, 'work': 16200, 'study': 3600, 'entertainment': 3600}, {'date': now.subtract(const Duration(days: 3)), 'total': 19800, 'work': 12600, 'study': 3600, 'entertainment': 3600}, {'date': now.subtract(const Duration(days: 2)), 'total': 27000, 'work': 19800, 'study': 3600, 'entertainment': 3600}, {'date': now.subtract(const Duration(days: 1)), 'total': 22500, 'work': 15300, 'study': 3600, 'entertainment': 3600}, {'date': now, 'total': 23040, 'work': 14400, 'study': 3600, 'entertainment': 3600}, ]; } else { // 月视图:显示30天数据(简化版,每天一个点) return List.generate(30, (index) { final date = now.subtract(Duration(days: 29 - index)); return { 'date': date, 'total': 18000 + (index % 7) * 2000, // 模拟每天5-7小时 'work': 12000 + (index % 7) * 1500, 'study': 3000 + (index % 7) * 300, 'entertainment': 3000 + (index % 7) * 200, }; }); } } @override Widget build(BuildContext context) { final theme = Theme.of(context); // 根据选中的时间段选择不同的 Provider final statsAsync = _selectedPeriod == '日' ? ref.watch(todayStatsListProvider) : _selectedPeriod == '周' ? ref.watch(weekStatsProvider) : ref.watch(monthStatsProvider); return Scaffold( appBar: AppBar( title: const Text('Statistics'), actions: [ IconButton( icon: const Icon(Icons.file_download), onPressed: () { // 导出数据 }, ), ], ), body: statsAsync.when( data: (stats) { final chartData = _getChartData(stats, _selectedPeriod); // 检查是否为空数据 final isEmpty = chartData.isEmpty || chartData.every((data) => (data['total'] as int) == 0); if (isEmpty) { return EmptyStateWidget.noData( title: '暂无统计数据', subtitle: '使用应用一段时间后,统计数据将显示在这里', actionLabel: '刷新', onAction: () { if (_selectedPeriod == '日') { ref.invalidate(todayStatsListProvider); } else if (_selectedPeriod == '周') { ref.invalidate(weekStatsProvider); } else { ref.invalidate(monthStatsProvider); } }, ); } return SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 时间选择器 _buildPeriodSelector(theme), const SizedBox(height: 24), // 时间趋势图 _buildTrendChart(theme, chartData, _selectedPeriod), const SizedBox(height: 24), // 分类对比图 _buildCategoryChart(theme, chartData, _selectedPeriod), const SizedBox(height: 24), // 应用使用详情 _buildAppDetails(theme), ], ), ); }, loading: () => const Center(child: CircularProgressIndicator()), error: (error, stack) => ErrorStateWidget.dataLoad( message: _getErrorMessage(error), onRetry: () { if (_selectedPeriod == '日') { ref.invalidate(todayStatsListProvider); } else if (_selectedPeriod == '周') { ref.invalidate(weekStatsProvider); } else { ref.invalidate(monthStatsProvider); } }, ), ), ); } Widget _buildPeriodSelector(ThemeData theme) { return Row( children: [ Expanded( child: SegmentedButton( segments: const [ ButtonSegment(value: '日', label: Text('日')), ButtonSegment(value: '周', label: Text('周')), ButtonSegment(value: '月', label: Text('月')), ], selected: {_selectedPeriod}, onSelectionChanged: (Set newSelection) { final newPeriod = newSelection.first; setState(() { _selectedPeriod = newPeriod; }); // 切换时间段时,刷新对应的 Provider if (newPeriod == '日') { ref.invalidate(todayStatsListProvider); } else if (newPeriod == '周') { ref.invalidate(weekStatsProvider); } else { ref.invalidate(monthStatsProvider); } }, ), ), const SizedBox(width: 12), IconButton( icon: const Icon(Icons.file_download), onPressed: () { // 导出功能 }, ), ], ); } Widget _buildTrendChart(ThemeData theme, List> chartData, String period) { return Container( height: 250, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: theme.cardColor, borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( period == '日' ? '每小时总时长趋势' : period == '周' ? '每日总时长趋势' : '每日总时长趋势(月)', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 16), Expanded( child: LineChart( LineChartData( gridData: FlGridData( show: true, drawVerticalLine: false, horizontalInterval: 2, getDrawingHorizontalLine: (value) { return FlLine( color: theme.colorScheme.onSurface.withOpacity(0.1), strokeWidth: 1, ); }, ), titlesData: FlTitlesData( leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 40, getTitlesWidget: (value, meta) { return Text( '${(value / 3600).toStringAsFixed(1)}h', style: TextStyle( fontSize: 10, color: theme.colorScheme.onSurface.withOpacity(0.6), ), ); }, ), ), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 30, getTitlesWidget: (value, meta) { if (value.toInt() >= 0 && value.toInt() < chartData.length) { final date = chartData[value.toInt()]['date'] as DateTime; String label; if (period == '日') { label = DateFormat('HH:mm', 'zh_CN').format(date); } else if (period == '周') { label = DateFormat('E', 'zh_CN').format(date); } else { label = DateFormat('M/d', 'zh_CN').format(date); } return Text( label, style: TextStyle( fontSize: 10, color: theme.colorScheme.onSurface.withOpacity(0.6), ), ); } return const Text(''); }, ), ), rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), ), borderData: FlBorderData(show: false), lineBarsData: [ LineChartBarData( spots: chartData.asMap().entries.map((entry) { return FlSpot( entry.key.toDouble(), (entry.value['total'] as int) / 3600.0, ); }).toList(), isCurved: true, color: AppTheme.primaryColor, barWidth: 3, dotData: const FlDotData(show: true), belowBarData: BarAreaData( show: true, color: AppTheme.primaryColor.withOpacity(0.1), ), ), ], ), ), ), ], ), ); } Widget _buildCategoryChart(ThemeData theme, List> chartData, String period) { return Container( height: 300, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: theme.cardColor, borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( period == '日' ? '每小时分类时间分布' : period == '周' ? '每日分类时间分布' : '每日分类时间分布(月)', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 16), Expanded( child: BarChart( BarChartData( alignment: BarChartAlignment.spaceAround, maxY: 8, barTouchData: BarTouchData(enabled: false), titlesData: FlTitlesData( leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 40, getTitlesWidget: (value, meta) { return Text( '${value.toInt()}h', style: TextStyle( fontSize: 10, color: theme.colorScheme.onSurface.withOpacity(0.6), ), ); }, ), ), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 30, getTitlesWidget: (value, meta) { if (value.toInt() >= 0 && value.toInt() < chartData.length) { final date = chartData[value.toInt()]['date'] as DateTime; String label; if (period == '日') { label = DateFormat('HH:mm', 'zh_CN').format(date); } else if (period == '周') { label = DateFormat('M/d', 'zh_CN').format(date); } else { label = DateFormat('M/d', 'zh_CN').format(date); } return Text( label, style: TextStyle( fontSize: 10, color: theme.colorScheme.onSurface.withOpacity(0.6), ), ); } return const Text(''); }, ), ), rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), ), gridData: FlGridData( show: true, drawVerticalLine: false, getDrawingHorizontalLine: (value) { return FlLine( color: theme.colorScheme.onSurface.withOpacity(0.1), strokeWidth: 1, ); }, ), borderData: FlBorderData(show: false), barGroups: chartData.asMap().entries.map((entry) { final data = entry.value; return BarChartGroupData( x: entry.key, barRods: [ BarChartRodData( toY: (data['work'] as int) / 3600.0, color: AppTheme.workColor, width: 12, borderRadius: const BorderRadius.vertical(top: Radius.circular(4)), ), BarChartRodData( toY: (data['study'] as int) / 3600.0, color: AppTheme.studyColor, width: 12, borderRadius: const BorderRadius.vertical(top: Radius.circular(4)), ), BarChartRodData( toY: (data['entertainment'] as int) / 3600.0, color: AppTheme.entertainmentColor, width: 12, borderRadius: const BorderRadius.vertical(top: Radius.circular(4)), ), ], ); }).toList(), ), ), ), ], ), ); } Widget _buildAppDetails(ThemeData theme) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '应用使用详情', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 12), _buildAppDetailItem('Chrome', '今日: 2h 15m', '本周: 12h 30m', '工作', theme), _buildAppDetailItem('VS Code', '今日: 1h 30m', '本周: 8h 45m', '工作', theme), _buildAppDetailItem('Slack', '今日: 1h', '本周: 6h', '工作', theme), ], ); } Widget _buildAppDetailItem( String appName, String todayTime, String weekTime, String category, ThemeData theme, ) { return Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: theme.cardColor, borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: AppTheme.getCategoryColor(category).withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: Icon( Icons.phone_android, color: AppTheme.getCategoryColor(category), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( appName, style: theme.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 4), Text( '分类: $category', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurface.withOpacity(0.6), ), ), ], ), ), ], ), const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '今日', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurface.withOpacity(0.6), ), ), const SizedBox(height: 4), Text( todayTime, style: theme.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, color: AppTheme.primaryColor, ), ), ], ), Container( width: 1, height: 40, color: theme.colorScheme.onSurface.withOpacity(0.1), ), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '本周', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurface.withOpacity(0.6), ), ), const SizedBox(height: 4), Text( weekTime, style: theme.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, color: AppTheme.primaryColor, ), ), ], ), ], ), ], ), ); } String _getErrorMessage(Object error) { final errorString = error.toString().toLowerCase(); if (errorString.contains('permission') || errorString.contains('权限')) { return '需要授予应用使用权限'; } else if (errorString.contains('network') || errorString.contains('网络')) { return '网络连接失败,请检查网络'; } else if (errorString.contains('database') || errorString.contains('数据库')) { return '数据库操作失败'; } return '加载失败,请稍后重试'; } }