feat(wakatime_screen): 增加日期范围选择功能并显示每日编码时间图表(#5)
- 添加日期范围选择器,支持今天、近三天和近一周的快速选择 - 实现开始日期和结束日期的单独选择功能 - 新增每日编码时间图表,展示所选日期范围内的编码时间分布 - 优化界面布局,提升用户体验
This commit is contained in:
parent
4cf1b4208a
commit
85d741e8d1
@ -24,6 +24,9 @@ class _WakatimeScreenState extends State<WakatimeScreen>
|
|||||||
String? _touchedChartType;
|
String? _touchedChartType;
|
||||||
late AnimationController _animationController;
|
late AnimationController _animationController;
|
||||||
late Animation<double> _animation;
|
late Animation<double> _animation;
|
||||||
|
DateTime _selectedStartDate = DateTime.now();
|
||||||
|
DateTime _selectedEndDate = DateTime.now();
|
||||||
|
int? _touchedDayIndex;
|
||||||
|
|
||||||
// 预定义的颜色列表
|
// 预定义的颜色列表
|
||||||
final List<Color> _colors = [
|
final List<Color> _colors = [
|
||||||
@ -54,14 +57,19 @@ class _WakatimeScreenState extends State<WakatimeScreen>
|
|||||||
);
|
);
|
||||||
_animationController.forward();
|
_animationController.forward();
|
||||||
|
|
||||||
// 设置默认日期为今天和一周前
|
// 设置默认日期为今天
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
_startDateController.text =
|
_selectedStartDate = now;
|
||||||
_formatDate(now.subtract(const Duration(days: 7)));
|
_selectedEndDate = now;
|
||||||
_endDateController.text = _formatDate(now);
|
_updateDateControllers();
|
||||||
_fetchData();
|
_fetchData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _updateDateControllers() {
|
||||||
|
_startDateController.text = _formatDate(_selectedStartDate);
|
||||||
|
_endDateController.text = _formatDate(_selectedEndDate);
|
||||||
|
}
|
||||||
|
|
||||||
String _formatDate(DateTime date) {
|
String _formatDate(DateTime date) {
|
||||||
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
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}分钟';
|
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 {
|
Future<void> _fetchData() async {
|
||||||
final authService = Provider.of<AuthService>(context, listen: false);
|
final authService = Provider.of<AuthService>(context, listen: false);
|
||||||
final token = authService.credentials['wakatime_token'];
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@ -339,30 +598,7 @@ class _WakatimeScreenState extends State<WakatimeScreen>
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// 日期选择
|
_buildDateRangeSelector(),
|
||||||
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',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// 获取数据按钮
|
// 获取数据按钮
|
||||||
@ -388,6 +624,10 @@ class _WakatimeScreenState extends State<WakatimeScreen>
|
|||||||
_buildTotalTime(),
|
_buildTotalTime(),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// 每日编码时间图表
|
||||||
|
_buildDailyChart(),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// 项目时间分布
|
// 项目时间分布
|
||||||
_buildProjectList(),
|
_buildProjectList(),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user