import 'package:flutter/material.dart'; import '../../constants/app_constants.dart'; /// 时间: 2026-03-26 /// 功能: 活跃页面 /// 介绍: 展示用户活跃度热力图,参考 GitHub 贡献图样式 /// 最新变化: 添加调试选项,修复布局溢出,调整活跃阈值颜色 class RatePage extends StatefulWidget { const RatePage({super.key}); @override State createState() => _RatePageState(); } class _RatePageState extends State with SingleTickerProviderStateMixin { late TabController _tabController; final List _tabs = ['周活跃', '月活跃']; // 调试参数 int _debugDayCount = 7; int _debugBarCount = 7; bool _showDebugPanel = false; // 活跃度阈值配置 final Map _activityThresholds = { 'low': 1, // 1-5 'medium': 6, // 6-20 'high': 21, // 21-100 'veryHigh': 100, // 100+ }; // 模拟活跃度数据 (实际条数) List _weekData = []; List> _monthData = []; @override void initState() { super.initState(); _tabController = TabController(length: _tabs.length, vsync: this); _generateMockData(); } @override void dispose() { _tabController.dispose(); super.dispose(); } // 生成模拟数据 void _generateMockData() { _weekData = List.generate(_debugDayCount, (index) { // 生成 0-150 的随机活跃值 return [0, 3, 8, 15, 35, 80, 150][index % 7]; }); _monthData = List.generate(4, (weekIndex) { return List.generate(_debugBarCount, (dayIndex) { return [0, 3, 8, 15, 35, 80, 150][(weekIndex * 7 + dayIndex) % 7]; }); }); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: Column( children: [ // Tab 切换栏 Container( decoration: BoxDecoration( color: Colors.white, border: Border(bottom: BorderSide(color: Colors.grey[200]!)), ), child: TabBar( controller: _tabController, tabs: _tabs.map((tab) => Tab(text: tab)).toList(), labelColor: AppConstants.primaryColor, unselectedLabelColor: Colors.grey[600], indicatorColor: AppConstants.primaryColor, indicatorWeight: 2, labelStyle: const TextStyle(fontWeight: FontWeight.w600), ), ), // 调试开关 _buildDebugToggle(), // 调试面板 if (_showDebugPanel) _buildDebugPanel(), // 内容区域 Expanded( child: TabBarView( controller: _tabController, children: [_buildWeekView(), _buildMonthView()], ), ), ], ), ); } // 调试开关 Widget _buildDebugToggle() { return Container( color: Colors.orange[50], padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ Icon(Icons.bug_report, color: Colors.orange[700], size: 20), const SizedBox(width: 8), Text( '开发中 - 调试模式', style: TextStyle( color: Colors.orange[700], fontWeight: FontWeight.w600, ), ), const Spacer(), TextButton.icon( onPressed: () { setState(() { _showDebugPanel = !_showDebugPanel; }); }, icon: Icon( _showDebugPanel ? Icons.expand_less : Icons.expand_more, size: 18, ), label: Text(_showDebugPanel ? '收起' : '展开'), style: TextButton.styleFrom( foregroundColor: Colors.orange[700], padding: const EdgeInsets.symmetric(horizontal: 12), ), ), ], ), ); } // 调试面板 Widget _buildDebugPanel() { return Container( color: Colors.orange[50], padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ // 天数调节 _buildDebugSlider( label: '天数', value: _debugDayCount.toDouble(), min: 3, max: 31, onChanged: (value) { setState(() { _debugDayCount = value.round(); _generateMockData(); }); }, ), const SizedBox(height: 12), // 条数调节 _buildDebugSlider( label: '条数', value: _debugBarCount.toDouble(), min: 3, max: 14, onChanged: (value) { setState(() { _debugBarCount = value.round(); _generateMockData(); }); }, ), const SizedBox(height: 16), // 活跃阈值说明 Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.orange[200]!), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '活跃阈值配置', style: TextStyle( fontWeight: FontWeight.w600, color: Colors.orange[700], ), ), const SizedBox(height: 8), _buildThresholdItem( '1-5', '浅色', AppConstants.primaryColor.withValues(alpha: 0.3), ), _buildThresholdItem( '6-20', '中浅色', AppConstants.primaryColor.withValues(alpha: 0.5), ), _buildThresholdItem( '21-100', '中深色', AppConstants.primaryColor.withValues(alpha: 0.7), ), _buildThresholdItem('100+', '最深色', AppConstants.primaryColor), ], ), ), ], ), ); } // 调试滑块 Widget _buildDebugSlider({ required String label, required double value, required double min, required double max, required ValueChanged onChanged, }) { return Row( children: [ SizedBox( width: 50, child: Text( label, style: TextStyle(fontSize: 14, color: Colors.orange[700]), ), ), Expanded( child: Slider( value: value, min: min, max: max, divisions: (max - min).round(), label: value.round().toString(), activeColor: Colors.orange[700], onChanged: onChanged, ), ), Container( width: 40, alignment: Alignment.center, child: Text( value.round().toString(), style: TextStyle( fontWeight: FontWeight.bold, color: Colors.orange[700], ), ), ), ], ); } // 阈值项 Widget _buildThresholdItem(String range, String label, Color color) { return Padding( padding: const EdgeInsets.only(bottom: 4), child: Row( children: [ Container( width: 16, height: 16, decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(4), ), ), const SizedBox(width: 8), Text( '$range: $label', style: TextStyle(fontSize: 12, color: Colors.grey[700]), ), ], ), ); } // 周活跃视图 Widget _buildWeekView() { final weekDays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']; return SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildSectionTitle('本周活跃趋势'), const SizedBox(height: 16), // 热力图 - 使用 Wrap 防止溢出 Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( children: [ // 星期标签 Wrap( spacing: 8, runSpacing: 8, alignment: WrapAlignment.center, children: List.generate(_debugDayCount, (index) { return SizedBox( width: 40, child: Text( weekDays[index % 7], textAlign: TextAlign.center, style: TextStyle( fontSize: 11, color: Colors.grey[600], fontWeight: FontWeight.w500, ), ), ); }), ), const SizedBox(height: 12), // 活跃度方块 Wrap( spacing: 8, runSpacing: 8, alignment: WrapAlignment.center, children: List.generate(_weekData.length, (index) { return _buildActivityBlock(_weekData[index], size: 40); }), ), ], ), ), const SizedBox(height: 16), _buildStatsSection(), const SizedBox(height: 16), _buildLegend(), ], ), ); } // 月活跃视图 Widget _buildMonthView() { return SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildSectionTitle('本月活跃热力图'), const SizedBox(height: 16), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( children: [ // 星期标签 Wrap( spacing: 4, alignment: WrapAlignment.center, children: ['一', '二', '三', '四', '五', '六', '日'] .map( (day) => SizedBox( width: 32, child: Text( day, textAlign: TextAlign.center, style: TextStyle( fontSize: 10, color: Colors.grey[500], ), ), ), ) .toList(), ), const SizedBox(height: 8), // 四周热力图 - 使用 Wrap 防止溢出 ...List.generate(4, (weekIndex) { return Padding( padding: const EdgeInsets.only(bottom: 6), child: Wrap( spacing: 4, alignment: WrapAlignment.center, children: List.generate(_monthData[weekIndex].length, ( dayIndex, ) { return _buildActivityBlock( _monthData[weekIndex][dayIndex], size: 28, ); }), ), ); }), ], ), ), const SizedBox(height: 16), _buildStatsSection(), const SizedBox(height: 16), _buildLegend(), ], ), ); } // 活跃度方块 Widget _buildActivityBlock(int value, {required double size}) { return Tooltip( message: '活跃度: $value', child: Container( width: size, height: size, decoration: BoxDecoration( color: _getActivityColor(value), borderRadius: BorderRadius.circular(4), border: value == 0 ? Border.all(color: Colors.grey[200]!) : null, ), child: value > 0 ? Center( child: value >= 100 ? Icon( Icons.local_fire_department, color: Colors.white.withValues(alpha: 0.9), size: size * 0.6, ) : null, ) : null, ), ); } // 获取活跃度颜色 (根据新的阈值) Color _getActivityColor(int value) { if (value == 0) { return Colors.grey[100]!; } else if (value >= 1 && value <= 5) { // 1-5: 浅色 return AppConstants.primaryColor.withValues(alpha: 0.3); } else if (value >= 6 && value <= 20) { // 6-20: 中浅色 return AppConstants.primaryColor.withValues(alpha: 0.5); } else if (value >= 21 && value <= 100) { // 21-100: 中深色 return AppConstants.primaryColor.withValues(alpha: 0.7); } else { // 100+: 最深色 return AppConstants.primaryColor; } } // 标题组件 Widget _buildSectionTitle(String title) { return Row( children: [ Container( width: 4, height: 20, decoration: BoxDecoration( color: AppConstants.primaryColor, borderRadius: BorderRadius.circular(2), ), ), const SizedBox(width: 8), Text( title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), ], ); } // 统计信息 Widget _buildStatsSection() { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _buildStatItem('活跃天数', '12', Icons.calendar_today), _buildStatItem('连续活跃', '5', Icons.local_fire_department), _buildStatItem('总活跃度', '89%', Icons.trending_up), ], ), ); } // 统计项 Widget _buildStatItem(String label, String value, IconData icon) { return Column( children: [ Icon(icon, color: AppConstants.primaryColor, size: 24), const SizedBox(height: 8), Text( value, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), const SizedBox(height: 4), Text(label, style: TextStyle(fontSize: 12, color: Colors.grey[600])), ], ); } // 图例说明 Widget _buildLegend() { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '活跃度说明', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600), ), const SizedBox(height: 12), Wrap( spacing: 12, runSpacing: 8, children: [ _buildLegendItem('无活跃', Colors.grey[100]!), _buildLegendItem( '1-5', AppConstants.primaryColor.withValues(alpha: 0.3), ), _buildLegendItem( '6-20', AppConstants.primaryColor.withValues(alpha: 0.5), ), _buildLegendItem( '21-100', AppConstants.primaryColor.withValues(alpha: 0.7), ), _buildLegendItem('100+', AppConstants.primaryColor), ], ), ], ), ); } // 图例项 Widget _buildLegendItem(String label, Color color) { return Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 12, height: 12, decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(2), border: label == '无活跃' ? Border.all(color: Colors.grey[300]!) : null, ), ), const SizedBox(width: 4), Text(label, style: TextStyle(fontSize: 11, color: Colors.grey[600])), ], ); } }