在这里插入图片描述

时间分析页的目标很明确:

  • 给用户一个“学习时间”的总览(今日/本周/本月/总计)
  • 再用一条折线图展示一周的学习趋势

这个页面属于 进度统计 模块,它更偏数据看板,而不是做题训练。
因此“可读性”和“信息层级”是最重要的设计目标。

本文涉及文件

  • lib/feature_pages.dart
  • lib/app.dart
  • lib/main.dart

1. 入口在哪里:从“进度统计”进入

时间分析属于 ProgressStatsPage(进度统计)里的一个入口。
入口页负责 push 到 TimeAnalysisPage,核心导航代码示例:

// 进度统计页中跳转至时间分析页的按钮点击事件
TextButton(
  onPressed: () {
    Navigator.push(
      context,
      MaterialPageRoute(builder: (_) => const TimeAnalysisPage()),
    );
  },
  child: const Text('查看时间分析'),
)
  • 采用 MaterialPageRoute 保证路由过渡动画符合平台规范
  • 跳转时传入 const TimeAnalysisPage() 利用常量构造器优化性能
  • 按钮文案“查看时间分析”语义清晰,符合用户操作预期

你的项目整体导航结构一直保持一致:

  • 聚合页负责入口
  • 子页负责实现

2. TimeAnalysisPage

下面这段实现来自项目 lib/feature_pages.dart

2.1 页面基础结构定义

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('时间分析')),
      body: Padding(
        padding: EdgeInsets.all(16.w),
        child: Column(
          children: [
  • 核心设计点1:使用 StatelessWidget 适配纯展示型统计页,无状态变更时性能更优
  • 核心设计点2:16.w 为响应式间距单位,适配不同屏幕宽度的鸿蒙/Flutter设备
  • 核心设计点3:Scaffold + AppBar 是Flutter标准页面骨架,保证导航栏一致性

2.2 页面标题与间距控制

            Text('学习时间分析', 
              style: TextStyle(
                fontSize: 20.sp, 
                fontWeight: FontWeight.bold
              )
            ),
            SizedBox(height: 24.h),
  • 标题字号 20.sp 采用响应式单位,比固定px更适配多尺寸设备
  • SizedBox(height: 24.h) 明确分隔标题与下方卡片,视觉呼吸感更佳
  • 标题加粗处理,强化页面核心主题的视觉层级

2.3 学习时间总览卡片容器

            Card(
              elevation: 2, // 补充阴影提升卡片层次感
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(8.w),
              ),
              child: Padding(
                padding: EdgeInsets.all(16.w),
                child: Column(
                  children: [
  • 新增 elevation: 2 给卡片添加轻微阴影,区分卡片与背景层
  • 新增 shape 属性设置圆角,符合移动端UI设计美学(8.w适配不同屏幕)
  • Card 组件包裹统计项,天然区分“总览数据”与“趋势图表”模块

2.4 时间统计项复用方法调用

                    _buildTimeItem('今日学习', '2小时15分钟'),
                    _buildTimeItem('本周学习', '12小时30分钟'),
                    _buildTimeItem('本月学习', '45小时20分钟'),
                    _buildTimeItem('总学习时间', '156小时45分钟'),
  • 复用 _buildTimeItem 方法,避免重复编写4次相同布局代码
  • 统计维度覆盖“今日-本周-本月-总计”,满足用户不同时间粒度的查看需求
  • 时间字符串格式统一为“X小时Y分钟”,降低用户理解成本

2.5 卡片闭合与趋势图标题

                  ],
                ),
              ),
            ),
            SizedBox(height: 24.h),
            Text('学习趋势', 
              style: TextStyle(
                fontSize: 18.sp, 
                fontWeight: FontWeight.bold
              )
            ),
            SizedBox(height: 16.h),
  • 卡片闭合后再次使用 SizedBox 分隔,保持模块间间距统一(24.h)
  • 趋势图标题字号 18.sp,略小于主标题,形成二级视觉层级
  • 趋势图标题与图表间间距16.h,比模块间距更小,视觉更紧凑

2.6 折线图核心布局与配置

            Expanded(
              child: LineChart(
                LineChartData(
                  lineBarsData: [
                    LineChartBarData(
                      spots: List.generate(7, (i) => 
                        FlSpot(i.toDouble(), (sin(i * 0.8) + 2) * 30)
                      ),
                      isCurved: true,
                      color: Colors.orange,
                      barWidth: 3, // 补充线条宽度
                      dotData: FlDotData(show: true), // 显示数据点
                    ),
                  ],
  • Expanded 让图表占据剩余空间,避免因屏幕尺寸不同导致图表高度异常
  • 新增 barWidth: 3 增加线条宽度,提升图表可读性
  • 新增 dotData: FlDotData(show: true) 显示数据点,用户可清晰看到每日数值
  • sin 函数生成演示数据,波动范围可控,模拟真实学习时长变化

2.7 图表坐标轴标题配置

                  titlesData: FlTitlesData(
                    bottomTitles: AxisTitles(sideTitles: SideTitles(
                      showTitles: true,
                      reservedSize: 24, // 预留标题空间
                      getTitlesWidget: (value, meta) => 
                        Text(['一', '二', '三', '四', '五', '六', '日'][value.toInt()]),
                    )),
                    leftTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
                  ),
                ),
              ),
            ),
  • 新增 reservedSize: 24 为底部星期标题预留空间,避免文字被截断
  • 隐藏左侧Y轴标题(leftTitles: showTitles: false),简化图表视觉,聚焦趋势
  • getTitlesWidget 映射索引到中文星期,符合中文用户使用习惯

2.8 页面布局闭合

          ],
        ),
      ),
    );
  }
  • 所有嵌套布局逐层闭合,保证代码语法正确性
  • Column、Padding、Scaffold 等组件嵌套逻辑清晰,符合Flutter布局规范

2.9 时间项复用方法实现

  Widget _buildTimeItem(String label, String time) {
    return Padding(
      padding: EdgeInsets.symmetric(vertical: 8.h),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(label, style: TextStyle(fontSize: 16.sp)),
          Text(time, 
            style: TextStyle(
              fontSize: 16.sp, 
              fontWeight: FontWeight.bold,
              color: Colors.blueAccent, // 补充时间文字颜色
            )
          ),
        ],
      ),
    );
  }
}
  • 核心复用逻辑:接收 labeltime 参数,适配不同统计项
  • MainAxisAlignment.spaceBetween 让标签左对齐、时间右对齐,布局整洁
  • 新增 color: Colors.blueAccent 给时间文字上色,强化核心信息视觉焦点
  • 垂直间距 8.h 保证每行统计项间距均匀,避免拥挤

3. 为什么用 StatelessWidget:这是展示型统计页

你把时间分析写成 StatelessWidget,补充核心原因:

class TimeAnalysisPage extends StatefulWidget {
  const TimeAnalysisPage({super.key});

  
  State<TimeAnalysisPage> createState() => _TimeAnalysisPageState();
}

class _TimeAnalysisPageState extends State<TimeAnalysisPage> {
  
  Widget build(BuildContext context) {
    return const Scaffold(body: Text('无状态变更时无需Stateful'));
  }
}
  • 核心原因1:当前代码中总览数据是写死的字符串,无动态变更需求
  • 核心原因2:趋势数据是用 sin 生成的演示数据,无需实时更新
  • 核心原因3:页面无筛选、切换等交互开关,无 setState 调用场景
  • 性能优势:StatelessWidget 无需维护状态对象,重建时开销更小

如果未来你要接入真实学习记录(例如按天累计学习时长),那时这页可能会变成:

  • Stateful(自己拉取/聚合数据)
  • 或者仍然 Stateless(由上层把统计结果注入)

4. 页面结构:上半部分总览,下半部分趋势

你用一个 Column 把页面拆成两块,补充布局优化细节:

Column(
  mainAxisSize: MainAxisSize.min, 
  crossAxisAlignment: CrossAxisAlignment.start, 
  children: [
  ],
)
  • 结构优势1:先给用户“概览结论”(总览卡片),符合用户“先看结果再看细节”的认知习惯
  • 结构优势2:再给用户“变化趋势”(折线图),满足深度查看需求
  • 布局兜底:新增 mainAxisSize: MainAxisSize.min 防止Column无限延伸导致溢出
  • 对齐优化:crossAxisAlignment: CrossAxisAlignment.start 让标题左对齐,更符合中文阅读习惯

5. 总览卡片:_buildTimeItem 抽取重复 UI

你没有重复写四个 Row,而是抽了一个方法,补充复用价值:

Padding(
  padding: EdgeInsets.symmetric(vertical: 8.h),
  child: Row(
    mainAxisAlignment: MainAxisAlignment.spaceBetween,
    children: [
      Text('今日学习', style: TextStyle(fontSize: 16.sp)),
      Text('2小时15分钟', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold)),
    ],
  ),
),
Padding(
  padding: EdgeInsets.symmetric(vertical: 8.h),
  child: Row(
    mainAxisAlignment: MainAxisAlignment.spaceBetween,
    children: [
      Text('本周学习', style: TextStyle(fontSize: 16.sp)),
      Text('12小时30分钟', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold)),
    ],
  ),
),

_buildTimeItem 方法内部做了三件事:

  • 用 Padding 给每一行留出上下间距
  • 用 Row 做左右布局
  • 左侧 label 普通字重,右侧 time 加粗且上色,强化核心信息
mainAxisAlignment: MainAxisAlignment.spaceBetween

这会让每一行像“数据表项”,非常适合统计页。


6. 为什么右侧 time 要加粗

你写了加粗样式,补充视觉设计原理:


Text(label, style: TextStyle(
  fontSize: 16.sp,
  fontWeight: FontWeight.normal, 
  color: Colors.grey[600], 
)),
Text(time, style: TextStyle(
  fontSize: 16.sp,
  fontWeight: FontWeight.bold,
  color: Colors.blueAccent, 
)),
  • 设计原理1:对比原则——加粗形成视觉焦点,用户扫一眼就能抓住关键数字
  • 设计原理2:层级原则——label 是说明(次要信息),time 是核心信息
  • 强化优化:新增灰色调给label、蓝色调给time,进一步拉大视觉层级
  • 一致性:所有time项统一加粗+上色,保持视觉风格一致

7. 趋势图:LineChart + 7 天数据

你用 LineChart 展示“学习趋势”,补充数据生成逻辑解析:

List<FlSpot> generateLearningSpots() {
  return List.generate(7, (i) {
    final sinValue = sin(i * 0.8);
    final positiveValue = sinValue + 2;
    final minutes = positiveValue * 30;
    return FlSpot(i.toDouble(), minutes);
  });
}
  • 数据逻辑1:sin(i * 0.8) 生成波动,0.8的系数让7天内波动2~3次,符合真实学习规律
  • 数据逻辑2:+ 2 把值抬高,避免出现负数学习时长(不符合业务逻辑)
  • 数据逻辑3:* 30 把数值放大到30~90分钟范围,映射到合理的单次学习时长
  • 复用思路:抽取 generateLearningSpots 方法,方便后续替换为真实数据

这个处理方式和你项目里“时间序列”页的思路是一致的:

  • 用可解释、可控的数学函数生成演示数据
  • 数据范围贴合业务场景,避免无意义的数值

8. isCurved:用平滑曲线强调趋势感

你设置了 isCurved: true,补充视觉效果对比:


LineChartBarData(
  isCurved: true, 
  // isCurved: false,
  curveSmoothness: 0.6,
),
  • 视觉效果1:平滑曲线更符合“趋势”的视觉认知,用户更容易感知整体走势
  • 视觉效果2:折线的尖角会分散注意力,让用户过度关注单日数值变化
  • 优化补充:curveSmoothness: 0.6 控制曲线平滑度,避免过度平滑导致失真
  • 业务适配:训练类App的统计模块,用户核心诉求是“整体学习规律”而非“单日精确值”

9. bottomTitles:用中文星期标注 x 轴

Widget buildWeekdayTitle(double value, TitleMeta meta) {
  final index = value.toInt();
  if (index < 0 || index >= 7) return const SizedBox();
  final weekdays = ['一', '二', '三', '四', '五', '六', '日'];
  return Text(weekdays[index]);
}
  • 核心价值1:把0~6的数字映射为中文星期,提升图表可读性
  • 核心价值2:只配置底部标题,避免多轴标签干扰视觉焦点
  • 安全优化:新增边界处理,防止value超出0~6范围导致数组越界
  • 复用优化:抽取 buildWeekdayTitle 方法,方便后续扩展多语言支持

需要注意的隐含约束是:

  • value 的范围必须在 0~6

你生成的 spots 也正好是 0~6,所以匹配。


10. Expanded:确保图表有足够空间

你把 LineChart 放进了 Expanded,补充布局对比:

Container(
  height: 200, 
  child: LineChart(...),
)

Expanded(
  flex: 1, 
  child: LineChart(...),
)
  • 布局优势1:避免图表高度固定导致的小屏挤压/大屏留白问题
  • 布局优势2:在不同屏幕比例(如16:9/18:9)下保持布局稳定
  • 业务适配:统计页中图表是核心内容,Expanded保证其优先级
  • 扩展灵活:可通过 flex 参数调整图表与其他模块的空间占比

11. 如何接入真实数据:从“每日学习记录”聚合

你现在的展示数据是字符串和 sin,补充真实数据接入示例:

class LearningTimeCalculator {
  static List<int> getDailyLearningMinutes() {
    return [45, 60, 30, 90, 75, 60, 80];
  }

  static String calculateToday() {
    final today = getDailyLearningMinutes().last;
    return '${today ~/ 60}小时${today % 60}分钟';
  }

  static String calculateWeek() {
    final total = getDailyLearningMinutes().reduce((a, b) => a + b);
    return '${total ~/ 60}小时${total % 60}分钟';
  }

  static List<FlSpot> convertToSpots() {
    return getDailyLearningMinutes().asMap().entries.map((e) {
      return FlSpot(e.key.toDouble(), e.value.toDouble());
    }).toList();
  }
}
  • 接入步骤1:定义数据模型,存储每日学习时长(分钟为单位,便于计算)
  • 接入步骤2:实现聚合方法,分别计算今日/本周/本月/总计时长
  • 接入步骤3:统一格式转换,将分钟数转为“X小时Y分钟”的展示字符串
  • 接入步骤4:映射图表数据,将每日分钟数转为FlSpot对象
  • 口径统一:所有统计维度使用分钟为底层单位,展示时统一格式化

12. 小结:时间分析实现的关键点

  • 总览清晰:Card + 4 行时间统计,新增阴影/圆角提升视觉层次
  • 复用到位:_buildTimeItem 抽取重复布局,减少冗余代码
  • 趋势可读:LineChart + 中文星期标签,新增数据点/线条宽度优化
  • 布局稳定:趋势图用 Expanded,适配不同屏幕尺寸
  • 扩展灵活:预留真实数据接入接口,便于后续业务迭代

到这里,这篇文章已经把你项目里的“时间分析”页面从代码结构到扩展方向讲清楚了.

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


Logo

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

更多推荐