feat(wakatime_screen): 增加日期范围选择功能并显示每日编码时间图表(#5)
- 添加日期范围选择器,支持今天、近三天和近一周的快速选择 - 实现开始日期和结束日期的单独选择功能 - 新增每日编码时间图表,展示所选日期范围内的编码时间分布 - 优化界面布局,提升用户体验
This commit is contained in:
parent
4cf1b4208a
commit
85d741e8d1
@ -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),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user