import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../../constants/app_constants.dart'; import '../../services/get/theme_controller.dart'; /// 时间: 2026-03-26 /// 功能: 活跃页面 /// 介绍: 展示用户活跃度热力图,参考 GitHub 贡献图样式 /// 最新变化: 2026-04-02 支持深色模式 class RatePage extends StatefulWidget { const RatePage({super.key}); @override State createState() => _RatePageState(); } class _RatePageState extends State with SingleTickerProviderStateMixin { late TabController _tabController; final List _tabs = ['周活跃', '月活跃']; final ThemeController _themeController = Get.find(); int _debugDayCount = 7; int _debugBarCount = 7; bool _showDebugPanel = false; 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) { 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 Obx(() { final isDark = _themeController.isDarkModeRx.value; return Scaffold( backgroundColor: isDark ? const Color(0xFF121212) : Theme.of(context).scaffoldBackgroundColor, body: Column( children: [ Container( decoration: BoxDecoration( color: isDark ? Colors.grey[900] : Colors.white, border: Border(bottom: BorderSide(color: isDark ? Colors.grey[800]! : Colors.grey[200]!)), ), child: TabBar( controller: _tabController, tabs: _tabs.map((tab) => Tab(text: tab)).toList(), labelColor: AppConstants.primaryColor, unselectedLabelColor: isDark ? Colors.grey[400] : Colors.grey[600], indicatorColor: AppConstants.primaryColor, indicatorWeight: 2, labelStyle: const TextStyle(fontWeight: FontWeight.w600), ), ), _buildDebugToggle(isDark), if (_showDebugPanel) _buildDebugPanel(isDark), Expanded( child: TabBarView( controller: _tabController, children: [_buildWeekView(isDark), _buildMonthView(isDark)], ), ), ], ), ); }); } Widget _buildDebugToggle(bool isDark) { return Container( color: isDark ? Colors.orange[900]!.withAlpha(50) : 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(bool isDark) { return Container( color: isDark ? Colors.orange[900]!.withAlpha(50) : 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, isDark: isDark, onChanged: (value) { setState(() { _debugDayCount = value.round(); _generateMockData(); }); }, ), const SizedBox(height: 12), _buildDebugSlider( label: '条数', value: _debugBarCount.toDouble(), min: 3, max: 14, isDark: isDark, onChanged: (value) { setState(() { _debugBarCount = value.round(); _generateMockData(); }); }, ), const SizedBox(height: 16), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: isDark ? Colors.grey[850] : 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.withAlpha(77), isDark, ), _buildThresholdItem( '6-20', '中浅色', AppConstants.primaryColor.withAlpha(128), isDark, ), _buildThresholdItem( '21-100', '中深色', AppConstants.primaryColor.withAlpha(179), isDark, ), _buildThresholdItem('100+', '最深色', AppConstants.primaryColor, isDark), ], ), ), ], ), ); } Widget _buildDebugSlider({ required String label, required double value, required double min, required double max, required bool isDark, 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, bool isDark) { 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: isDark ? Colors.grey[300] : Colors.grey[700]), ), ], ), ); } Widget _buildWeekView(bool isDark) { final weekDays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']; return SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildSectionTitle('本周活跃趋势(生成图片分享)', isDark), const SizedBox(height: 16), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: isDark ? Colors.grey[850] : Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withAlpha(isDark ? 0 : 13), 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: isDark ? Colors.grey[400] : 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, isDark: isDark); }), ), ], ), ), const SizedBox(height: 16), _buildStatsSection(isDark), const SizedBox(height: 16), _buildLegend(isDark), ], ), ); } Widget _buildMonthView(bool isDark) { return SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildSectionTitle('本月活跃热力图', isDark), const SizedBox(height: 16), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: isDark ? Colors.grey[850] : Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withAlpha(isDark ? 0 : 13), 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: isDark ? Colors.grey[500] : Colors.grey[500], ), ), ), ) .toList(), ), const SizedBox(height: 8), ...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, isDark: isDark, ); }), ), ); }), ], ), ), const SizedBox(height: 16), _buildStatsSection(isDark), const SizedBox(height: 16), _buildLegend(isDark), ], ), ); } Widget _buildActivityBlock(int value, {required double size, required bool isDark}) { 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: isDark ? Colors.grey[700]! : Colors.grey[200]!) : null, ), child: value > 0 ? Center( child: value >= 100 ? Icon( Icons.local_fire_department, color: Colors.white.withAlpha(230), size: size * 0.6, ) : null, ) : null, ), ); } Color _getActivityColor(int value) { if (value == 0) { return Colors.grey[100]!; } else if (value >= 1 && value <= 5) { return AppConstants.primaryColor.withAlpha(77); } else if (value >= 6 && value <= 20) { return AppConstants.primaryColor.withAlpha(128); } else if (value >= 21 && value <= 100) { return AppConstants.primaryColor.withAlpha(179); } else { return AppConstants.primaryColor; } } Widget _buildSectionTitle(String title, bool isDark) { return Row( children: [ Container( width: 4, height: 20, decoration: BoxDecoration( color: AppConstants.primaryColor, borderRadius: BorderRadius.circular(2), ), ), const SizedBox(width: 8), Text( title, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: isDark ? Colors.white : Colors.black), ), ], ); } Widget _buildStatsSection(bool isDark) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: isDark ? Colors.grey[850] : Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withAlpha(isDark ? 0 : 13), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _buildStatItem('活跃天数', '12', Icons.calendar_today, isDark), _buildStatItem('连续活跃', '5', Icons.local_fire_department, isDark), _buildStatItem('总活跃度', '89%', Icons.trending_up, isDark), ], ), ); } Widget _buildStatItem(String label, String value, IconData icon, bool isDark) { return Column( children: [ Icon(icon, color: AppConstants.primaryColor, size: 24), const SizedBox(height: 8), Text( value, style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: isDark ? Colors.white : Colors.black), ), const SizedBox(height: 4), Text(label, style: TextStyle(fontSize: 12, color: isDark ? Colors.grey[400] : Colors.grey[600])), ], ); } Widget _buildLegend(bool isDark) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: isDark ? Colors.grey[850] : Colors.white, borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '活跃度说明', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: isDark ? Colors.white : Colors.black), ), const SizedBox(height: 12), Wrap( spacing: 12, runSpacing: 8, children: [ _buildLegendItem('无活跃', Colors.grey[100]!, isDark), _buildLegendItem('1-5', AppConstants.primaryColor.withAlpha(77), isDark), _buildLegendItem('6-20', AppConstants.primaryColor.withAlpha(128), isDark), _buildLegendItem('21-100', AppConstants.primaryColor.withAlpha(179), isDark), _buildLegendItem('100+', AppConstants.primaryColor, isDark), ], ), ], ), ); } Widget _buildLegendItem(String label, Color color, bool isDark) { return Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 12, height: 12, decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(2), border: label == '无活跃' ? Border.all(color: isDark ? Colors.grey[600]! : Colors.grey[300]!) : null, ), ), const SizedBox(width: 4), Text(label, style: TextStyle(fontSize: 11, color: isDark ? Colors.grey[400] : Colors.grey[600])), ], ); } }