596 lines
17 KiB
Dart
596 lines
17 KiB
Dart
import 'package:flutter/material.dart';
|
|
import '../../constants/app_constants.dart';
|
|
|
|
/// 时间: 2026-03-26
|
|
/// 功能: 活跃页面
|
|
/// 介绍: 展示用户活跃度热力图,参考 GitHub 贡献图样式
|
|
/// 最新变化: 添加调试选项,修复布局溢出,调整活跃阈值颜色
|
|
|
|
class RatePage extends StatefulWidget {
|
|
const RatePage({super.key});
|
|
|
|
@override
|
|
State<RatePage> createState() => _RatePageState();
|
|
}
|
|
|
|
class _RatePageState extends State<RatePage>
|
|
with SingleTickerProviderStateMixin {
|
|
late TabController _tabController;
|
|
final List<String> _tabs = ['周活跃', '月活跃'];
|
|
|
|
// 调试参数
|
|
int _debugDayCount = 7;
|
|
int _debugBarCount = 7;
|
|
bool _showDebugPanel = false;
|
|
|
|
// 模拟活跃度数据 (实际条数)
|
|
List<int> _weekData = [];
|
|
List<List<int>> _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<double> 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])),
|
|
],
|
|
);
|
|
}
|
|
}
|