feat(wakatime_screen): 增加日期范围选择功能并显示每日编码时间图表(#5)

- 添加日期范围选择器,支持今天、近三天和近一周的快速选择
- 实现开始日期和结束日期的单独选择功能
- 新增每日编码时间图表,展示所选日期范围内的编码时间分布
- 优化界面布局,提升用户体验
This commit is contained in:
高手 2025-06-10 14:34:26 +08:00
parent 4cf1b4208a
commit 85d741e8d1

View File

@ -24,6 +24,9 @@ class _WakatimeScreenState extends State<WakatimeScreen>
String? _touchedChartType;
late AnimationController _animationController;
late Animation<double> _animation;
DateTime _selectedStartDate = DateTime.now();
DateTime _selectedEndDate = DateTime.now();
int? _touchedDayIndex;
//
final List<Color> _colors = [
@ -54,14 +57,19 @@ class _WakatimeScreenState extends State<WakatimeScreen>
);
_animationController.forward();
//
//
final now = DateTime.now();
_startDateController.text =
_formatDate(now.subtract(const Duration(days: 7)));
_endDateController.text = _formatDate(now);
_selectedStartDate = now;
_selectedEndDate = now;
_updateDateControllers();
_fetchData();
}
void _updateDateControllers() {
_startDateController.text = _formatDate(_selectedStartDate);
_endDateController.text = _formatDate(_selectedEndDate);
}
String _formatDate(DateTime date) {
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
}
@ -72,6 +80,26 @@ class _WakatimeScreenState extends State<WakatimeScreen>
return '${hours}小时${minutes}分钟';
}
void _selectDateRange(String range) {
final now = DateTime.now();
switch (range) {
case 'today':
_selectedStartDate = now;
_selectedEndDate = now;
break;
case '3days':
_selectedStartDate = now.subtract(const Duration(days: 2));
_selectedEndDate = now;
break;
case 'week':
_selectedStartDate = now.subtract(const Duration(days: 6));
_selectedEndDate = now;
break;
}
_updateDateControllers();
_fetchData();
}
Future<void> _fetchData() async {
final authService = Provider.of<AuthService>(context, listen: false);
final token = authService.credentials['wakatime_token'];
@ -328,6 +356,237 @@ class _WakatimeScreenState extends State<WakatimeScreen>
);
}
Widget _buildDateRangeSelector() {
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildDateRangeButton('今天', 'today'),
_buildDateRangeButton('近三天', '3days'),
_buildDateRangeButton('近一周', 'week'),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: TextField(
controller: _startDateController,
readOnly: true,
decoration: const InputDecoration(
labelText: '开始日期',
hintText: 'YYYY-MM-DD',
suffixIcon: Icon(Icons.calendar_today),
),
onTap: () async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: _selectedStartDate,
firstDate:
DateTime.now().subtract(const Duration(days: 365)),
lastDate: DateTime.now(),
);
if (picked != null) {
setState(() {
_selectedStartDate = picked;
if (picked.isAfter(_selectedEndDate)) {
_selectedEndDate = picked;
_endDateController.text = _formatDate(picked);
}
_startDateController.text = _formatDate(picked);
_fetchData();
});
}
},
),
),
const SizedBox(width: 16),
Expanded(
child: TextField(
controller: _endDateController,
readOnly: true,
decoration: const InputDecoration(
labelText: '结束日期',
hintText: 'YYYY-MM-DD',
suffixIcon: Icon(Icons.calendar_today),
),
onTap: () async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: _selectedEndDate,
firstDate: _selectedStartDate,
lastDate: DateTime.now(),
);
if (picked != null) {
setState(() {
_selectedEndDate = picked;
if (picked.isBefore(_selectedStartDate)) {
_selectedStartDate = picked;
_startDateController.text = _formatDate(picked);
}
_endDateController.text = _formatDate(picked);
_fetchData();
});
}
},
),
),
],
),
],
);
}
Widget _buildDateRangeButton(String text, String range) {
final isSelected =
(range == 'today' && _selectedStartDate == _selectedEndDate) ||
(range == '3days' &&
_selectedEndDate.difference(_selectedStartDate).inDays == 2) ||
(range == 'week' &&
_selectedEndDate.difference(_selectedStartDate).inDays == 6);
return ElevatedButton(
onPressed: () => _selectDateRange(range),
style: ElevatedButton.styleFrom(
backgroundColor: isSelected ? Theme.of(context).primaryColor : null,
foregroundColor: isSelected ? Colors.white : null,
),
child: Text(text),
);
}
Widget _buildDailyChart() {
if (_summary == null || _summary!.data.length <= 1)
return const SizedBox.shrink();
final dailyData = _summary!.data;
final maxSeconds = dailyData
.map((d) => d.grandTotal.totalSeconds)
.reduce((a, b) => a > b ? a : b);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'每日编码时间',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
SizedBox(
height: 200,
child: BarChart(
BarChartData(
alignment: BarChartAlignment.spaceAround,
maxY: maxSeconds.toDouble(),
barTouchData: BarTouchData(
touchCallback: (FlTouchEvent event, response) {
if (event.isInterestedForInteractions &&
response != null &&
response.spot != null) {
setState(() {
_touchedDayIndex = response.spot!.touchedBarGroupIndex;
});
} else {
setState(() {
_touchedDayIndex = null;
});
}
},
handleBuiltInTouches: false,
touchTooltipData: BarTouchTooltipData(
tooltipBgColor: Colors.blueGrey,
getTooltipItem: (group, groupIndex, rod, rodIndex) {
final date =
DateTime.parse(dailyData[groupIndex].range.date);
final seconds = rod.toY.toInt();
return BarTooltipItem(
'${_formatDate(date)}\n${_formatDuration(seconds)}',
const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
);
},
),
),
titlesData: FlTitlesData(
show: true,
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
leftTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) {
if (value >= 0 && value < dailyData.length) {
final date =
DateTime.parse(dailyData[value.toInt()].range.date);
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
'${date.month}/${date.day}',
style: const TextStyle(fontSize: 10),
),
);
}
return const Text('');
},
),
),
rightTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) {
return Text(
'${(value / 3600).round()}h',
style: const TextStyle(fontSize: 10),
);
},
),
),
),
borderData: FlBorderData(show: false),
gridData: FlGridData(show: false),
barGroups: dailyData.asMap().entries.map((entry) {
final index = entry.key;
final day = entry.value;
final isTouched = index == _touchedDayIndex;
return BarChartGroupData(
x: index,
barRods: [
BarChartRodData(
toY: day.grandTotal.totalSeconds.toDouble(),
width: 20,
color: isTouched
? Theme.of(context).primaryColor
: Theme.of(context).primaryColor.withOpacity(0.5),
),
],
);
}).toList(),
),
),
),
if (_touchedDayIndex != null) ...[
const SizedBox(height: 16),
Center(
child: Text(
'${_formatDate(DateTime.parse(dailyData[_touchedDayIndex!].range.date))}: ${_formatDuration(dailyData[_touchedDayIndex!].grandTotal.totalSeconds.toInt())}',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
],
],
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -339,30 +598,7 @@ class _WakatimeScreenState extends State<WakatimeScreen>
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//
Row(
children: [
Expanded(
child: TextField(
controller: _startDateController,
decoration: const InputDecoration(
labelText: '开始日期',
hintText: 'YYYY-MM-DD',
),
),
),
const SizedBox(width: 16),
Expanded(
child: TextField(
controller: _endDateController,
decoration: const InputDecoration(
labelText: '结束日期',
hintText: 'YYYY-MM-DD',
),
),
),
],
),
_buildDateRangeSelector(),
const SizedBox(height: 16),
//
@ -388,6 +624,10 @@ class _WakatimeScreenState extends State<WakatimeScreen>
_buildTotalTime(),
const SizedBox(height: 24),
//
_buildDailyChart(),
const SizedBox(height: 24),
//
_buildProjectList(),
const SizedBox(height: 24),