From 85d741e8d1eb6390c30fd62ffda65f8301ec6d10 Mon Sep 17 00:00:00 2001 From: jdysya <1912377458@qq.com> Date: Tue, 10 Jun 2025 14:34:26 +0800 Subject: [PATCH] =?UTF-8?q?feat(wakatime=5Fscreen):=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E6=97=A5=E6=9C=9F=E8=8C=83=E5=9B=B4=E9=80=89=E6=8B=A9=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=B9=B6=E6=98=BE=E7=A4=BA=E6=AF=8F=E6=97=A5=E7=BC=96?= =?UTF-8?q?=E7=A0=81=E6=97=B6=E9=97=B4=E5=9B=BE=E8=A1=A8(#5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加日期范围选择器,支持今天、近三天和近一周的快速选择 - 实现开始日期和结束日期的单独选择功能 - 新增每日编码时间图表,展示所选日期范围内的编码时间分布 - 优化界面布局,提升用户体验 --- lib/screens/wakatime_screen.dart | 296 ++++++++++++++++++++++++++++--- 1 file changed, 268 insertions(+), 28 deletions(-) diff --git a/lib/screens/wakatime_screen.dart b/lib/screens/wakatime_screen.dart index 8a14c91..6280af6 100644 --- a/lib/screens/wakatime_screen.dart +++ b/lib/screens/wakatime_screen.dart @@ -24,6 +24,9 @@ class _WakatimeScreenState extends State String? _touchedChartType; late AnimationController _animationController; late Animation _animation; + DateTime _selectedStartDate = DateTime.now(); + DateTime _selectedEndDate = DateTime.now(); + int? _touchedDayIndex; // 预定义的颜色列表 final List _colors = [ @@ -54,14 +57,19 @@ class _WakatimeScreenState extends State ); _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 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 _fetchData() async { final authService = Provider.of(context, listen: false); final token = authService.credentials['wakatime_token']; @@ -328,6 +356,237 @@ class _WakatimeScreenState extends State ); } + 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 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 _buildTotalTime(), const SizedBox(height: 24), + // 每日编码时间图表 + _buildDailyChart(), + const SizedBox(height: 24), + // 项目时间分布 _buildProjectList(), const SizedBox(height: 24),