在这里插入图片描述

前言

数据可视化是现代健康类应用的重要特性。通过周报告功能,用户可以直观地了解自己一周的口腔护理情况,包括刷牙次数、护理完成度、连续坚持天数等关键指标。这种数据反馈能够有效激励用户保持良好的护理习惯。

本文将详细介绍如何使用 Flutter 和 fl_chart 图表库实现一个功能丰富的周报告页面。

功能需求

周报告页面需要展示以下内容:

  • 周期信息:显示当前报告的时间范围
  • 核心指标:刷牙次数、日均刷牙、连续天数、获得积分
  • 趋势图表:使用柱状图展示一周内每天的刷牙次数
  • 完成情况:展示刷牙、漱口、牙线等护理项目的完成进度
  • 个性化建议:根据数据给出针对性的改进建议

页面基础结构

周报告页面使用 StatelessWidget 实现,数据通过 ConsumerAppProvider 获取:

class WeeklyReportPage extends StatelessWidget {
  const WeeklyReportPage({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('周报告')),
      body: Consumer<AppProvider>(
        builder: (context, provider, _) {
          final weeklyData = provider.getWeeklyBrushData();
          final totalBrush = weeklyData.reduce((a, b) => a + b);
          final avgBrush = (totalBrush / 7).toStringAsFixed(1);

在构建界面之前,先从 Provider 获取周数据并计算统计值。reduce 方法用于计算数组元素的总和,toStringAsFixed(1) 保留一位小数。

周期信息卡片

页面顶部展示报告的时间范围:

          return SingleChildScrollView(
            padding: const EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Container(
                  padding: const EdgeInsets.all(16),
                  decoration: BoxDecoration(
                    color: const Color(0xFF26A69A),
                    borderRadius: BorderRadius.circular(12),
                  ),
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          const Text('本周报告', 
                              style: TextStyle(color: Colors.white, 
                                  fontSize: 20, fontWeight: FontWeight.bold)),
                          Text(
                            '${DateFormat('MM/dd').format(DateTime.now().subtract(const Duration(days: 6)))} - ${DateFormat('MM/dd').format(DateTime.now())}',
                            style: const TextStyle(color: Colors.white70),
                          ),
                        ],
                      ),
                      const Icon(Icons.analytics, color: Colors.white, size: 40),
                    ],
                  ),
                ),

使用主题色作为背景,白色文字形成对比。日期范围通过 DateFormat 格式化,显示从6天前到今天的时间段。

统计卡片网格

核心指标使用两行两列的网格布局展示:

                const SizedBox(height: 20),
                Row(
                  children: [
                    Expanded(child: _buildStatCard('刷牙次数', '$totalBrush次', Icons.brush)),
                    const SizedBox(width: 12),
                    Expanded(child: _buildStatCard('日均刷牙', '$avgBrush次', Icons.trending_up)),
                  ],
                ),
                const SizedBox(height: 12),
                Row(
                  children: [
                    Expanded(child: _buildStatCard('连续天数', '${provider.streakDays}天', Icons.local_fire_department)),
                    const SizedBox(width: 12),
                    Expanded(child: _buildStatCard('获得积分', '+${provider.totalPoints ~/ 4}', Icons.stars)),
                  ],
                ),

使用 RowExpanded 组合实现等宽的两列布局,每个统计项占据一半宽度。

统计卡片组件的实现:

Widget _buildStatCard(String label, String value, IconData icon) {
  return Container(
    padding: const EdgeInsets.all(16),
    decoration: BoxDecoration(
      color: Colors.white,
      borderRadius: BorderRadius.circular(12),
    ),
    child: Row(
      children: [
        Container(
          padding: const EdgeInsets.all(8),
          decoration: BoxDecoration(
            color: const Color(0xFF26A69A).withOpacity(0.1),
            shape: BoxShape.circle,
          ),
          child: Icon(icon, color: const Color(0xFF26A69A), size: 20),
        ),
        const SizedBox(width: 12),
        Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(value, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
            Text(label, style: TextStyle(color: Colors.grey.shade600, fontSize: 12)),
          ],
        ),
      ],
    ),
  );
}

每个卡片包含图标、数值和标签三个元素。图标使用圆形浅色背景,数值使用大号加粗字体突出显示。

刷牙趋势图表

使用 fl_chart 库绘制柱状图展示一周的刷牙趋势:

                const SizedBox(height: 24),
                const Text('刷牙趋势', 
                    style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                const SizedBox(height: 12),
                Container(
                  height: 200,
                  padding: const EdgeInsets.all(16),
                  decoration: BoxDecoration(
                    color: Colors.white,
                    borderRadius: BorderRadius.circular(12),
                  ),
                  child: BarChart(
                    BarChartData(
                      alignment: BarChartAlignment.spaceAround,
                      maxY: 5,
                      barTouchData: BarTouchData(enabled: false),

图表容器设置固定高度200像素,maxY 设为5表示Y轴最大值,barTouchData 禁用触摸交互简化实现。

底部标题配置,显示周一到周日:

                      titlesData: FlTitlesData(
                        show: true,
                        bottomTitles: AxisTitles(
                          sideTitles: SideTitles(
                            showTitles: true,
                            getTitlesWidget: (value, meta) {
                              final days = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
                              return Text(days[value.toInt()], 
                                  style: const TextStyle(fontSize: 10));
                            },
                          ),
                        ),

getTitlesWidget 回调函数根据索引返回对应的星期文字,字体设为10像素保持紧凑。

左侧标题配置,显示数值刻度:

                        leftTitles: AxisTitles(
                          sideTitles: SideTitles(
                            showTitles: true,
                            reservedSize: 30,
                            getTitlesWidget: (value, meta) {
                              return Text('${value.toInt()}', 
                                  style: const TextStyle(fontSize: 10));
                            },
                          ),
                        ),
                        topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
                        rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
                      ),
                      borderData: FlBorderData(show: false),

reservedSize 为左侧标题预留30像素宽度,顶部和右侧标题隐藏,边框也隐藏以保持简洁。

柱状图数据生成:

                      barGroups: List.generate(7, (index) {
                        return BarChartGroupData(
                          x: index,
                          barRods: [
                            BarChartRodData(
                              toY: weeklyData[index].toDouble(),
                              color: const Color(0xFF26A69A),
                              width: 20,
                              borderRadius: const BorderRadius.vertical(top: Radius.circular(4)),
                            ),
                          ],
                        );
                      }),
                    ),
                  ),
                ),

使用 List.generate 生成7个柱状图组,每个柱子的高度对应当天的刷牙次数。柱子宽度20像素,顶部圆角4像素。

护理完成情况

展示各项护理的完成进度:

                const SizedBox(height: 24),
                const Text('护理完成情况', 
                    style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                const SizedBox(height: 12),
                _buildCompletionItem('刷牙', totalBrush, 21, const Color(0xFF26A69A)),
                _buildCompletionItem('漱口', provider.mouthwashRecords.length, 14, const Color(0xFF42A5F5)),
                _buildCompletionItem('牙线', provider.flossRecords.length, 7, const Color(0xFFAB47BC)),

三种护理类型使用不同的颜色区分:刷牙绿色、漱口蓝色、牙线紫色。目标值分别是21次、14次、7次。

完成进度组件的实现:

Widget _buildCompletionItem(String label, int current, int target, Color color) {
  final percent = (current / target).clamp(0.0, 1.0);
  return Container(
    margin: const EdgeInsets.only(bottom: 12),
    padding: const EdgeInsets.all(16),
    decoration: BoxDecoration(
      color: Colors.white,
      borderRadius: BorderRadius.circular(12),
    ),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text(label, style: const TextStyle(fontWeight: FontWeight.bold)),
            Text('$current/$target', style: TextStyle(color: Colors.grey.shade600)),
          ],
        ),

首先计算完成百分比,使用 clamp 确保值在0到1之间。顶部显示标签和完成数量。

进度条的实现:

        const SizedBox(height: 8),
        LinearProgressIndicator(
          value: percent,
          backgroundColor: Colors.grey.shade200,
          valueColor: AlwaysStoppedAnimation<Color>(color),
          minHeight: 8,
          borderRadius: BorderRadius.circular(4),
        ),
      ],
    ),
  );
}

使用 LinearProgressIndicator 组件显示进度,设置最小高度8像素和圆角4像素,让进度条更加美观。

个性化建议

根据用户的护理数据给出针对性建议:

                const SizedBox(height: 24),
                Container(
                  padding: const EdgeInsets.all(16),
                  decoration: BoxDecoration(
                    color: Colors.amber.shade50,
                    borderRadius: BorderRadius.circular(12),
                  ),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Row(
                        children: [
                          Icon(Icons.lightbulb, color: Colors.amber.shade700),
                          const SizedBox(width: 8),
                          const Text('本周建议', 
                              style: TextStyle(fontWeight: FontWeight.bold)),
                        ],
                      ),
                      const SizedBox(height: 8),
                      Text(
                        _getWeeklyAdvice(totalBrush, 
                            provider.mouthwashRecords.length, 
                            provider.flossRecords.length),
                        style: TextStyle(color: Colors.grey.shade700),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          );
        },
      ),
    );
  }

建议卡片使用琥珀色浅色背景,配合灯泡图标,营造出温馨的提示氛围。

建议生成逻辑:

String _getWeeklyAdvice(int brush, int mouthwash, int floss) {
  if (brush >= 21 && mouthwash >= 14 && floss >= 7) {
    return '太棒了!本周口腔护理非常到位,继续保持这个好习惯!';
  } else if (brush >= 14) {
    return '刷牙习惯不错,建议增加漱口水和牙线的使用频率,让口腔护理更全面。';
  } else {
    return '本周刷牙次数偏少,建议每天至少刷牙2次,每次2-3分钟,保护牙齿健康。';
  }
}

根据三项护理的完成情况,返回不同的建议文案。这种个性化的反馈能够帮助用户了解自己的不足并改进。

Provider 数据方法

AppProvider 中实现获取周数据的方法:

List<int> getWeeklyBrushData() {
  final now = DateTime.now();
  final weekData = <int>[];
  
  for (int i = 6; i >= 0; i--) {
    final date = now.subtract(Duration(days: i));
    final count = _brushRecords.where((record) {
      return record.date.year == date.year &&
             record.date.month == date.month &&
             record.date.day == date.day;
    }).length;
    weekData.add(count);
  }
  
  return weekData;
}

遍历最近7天,统计每天的刷牙记录数量。使用日期比较确保只统计当天的记录。

连续天数的计算:

int get streakDays {
  if (_brushRecords.isEmpty) return 0;
  
  int streak = 0;
  var checkDate = DateTime.now();
  
  while (true) {
    final hasRecord = _brushRecords.any((record) {
      return record.date.year == checkDate.year &&
             record.date.month == checkDate.month &&
             record.date.day == checkDate.day;
    });
    
    if (hasRecord) {
      streak++;
      checkDate = checkDate.subtract(const Duration(days: 1));
    } else {
      break;
    }
  }
  
  return streak;
}

从今天开始往前检查,只要当天有刷牙记录就增加连续天数,遇到没有记录的日期就停止。

fl_chart 依赖配置

使用 fl_chart 需要在 pubspec.yaml 中添加依赖:

dependencies:
  fl_chart: ^0.65.0

然后在代码中导入:

import 'package:fl_chart/fl_chart.dart';

fl_chart 是一个功能强大的 Flutter 图表库,支持折线图、柱状图、饼图等多种图表类型。

图表交互增强

如果需要添加触摸交互,可以配置 barTouchData

barTouchData: BarTouchData(
  enabled: true,
  touchTooltipData: BarTouchTooltipData(
    tooltipBgColor: Colors.blueGrey,
    getTooltipItem: (group, groupIndex, rod, rodIndex) {
      final days = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
      return BarTooltipItem(
        '${days[group.x]}\n${rod.toY.toInt()}次',
        const TextStyle(color: Colors.white),
      );
    },
  ),
),

配置后用户点击柱子时会显示详细信息的提示框,包含星期和具体次数。

动画效果

fl_chart 支持图表动画,可以通过 swapAnimationDuration 配置:

BarChart(
  BarChartData(...),
  swapAnimationDuration: const Duration(milliseconds: 300),
  swapAnimationCurve: Curves.easeInOut,
)

当数据变化时,图表会以动画形式过渡到新状态,提升用户体验。

数据导出功能思路

周报告可以考虑添加导出功能,让用户保存或分享自己的护理数据:

Future<void> exportReport() async {
  final reportData = {
    'period': '${startDate} - ${endDate}',
    'totalBrush': totalBrush,
    'avgBrush': avgBrush,
    'streakDays': streakDays,
    'dailyData': weeklyData,
  };
  
  // 转换为 JSON 或生成 PDF
  final jsonStr = jsonEncode(reportData);
  // 保存或分享
}

可以将数据导出为 JSON 格式,或者使用 pdf 库生成 PDF 报告。

性能优化建议

对于周报告这种数据密集型页面,可以考虑以下优化:

缓存计算结果:周数据的计算可以缓存起来,只在数据变化时重新计算。

List<int>? _cachedWeeklyData;
DateTime? _cacheDate;

List<int> getWeeklyBrushData() {
  final today = DateTime.now();
  if (_cachedWeeklyData != null && 
      _cacheDate?.day == today.day) {
    return _cachedWeeklyData!;
  }
  
  // 重新计算
  _cachedWeeklyData = _calculateWeeklyData();
  _cacheDate = today;
  return _cachedWeeklyData!;
}

延迟加载:如果数据量大,可以先显示骨架屏,数据加载完成后再渲染图表。

响应式设计

为了适配不同屏幕尺寸,可以使用 LayoutBuilder 动态调整图表高度:

LayoutBuilder(
  builder: (context, constraints) {
    final chartHeight = constraints.maxWidth * 0.5;
    return Container(
      height: chartHeight,
      child: BarChart(...),
    );
  },
)

图表高度设为宽度的一半,在不同设备上保持合适的比例。

总结

本文详细介绍了口腔护理 App 中周报告功能的实现。通过 fl_chart 图表库和合理的数据处理,我们构建了一个信息丰富、视觉美观的数据报告页面。核心技术点包括:

  • 使用 fl_chart 绘制柱状图展示趋势数据
  • 通过 LinearProgressIndicator 展示完成进度
  • 根据数据动态生成个性化建议
  • 合理的数据计算和缓存策略

数据可视化是提升用户粘性的有效手段,希望本文对你实现类似功能有所启发。

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐