请添加图片描述

前言

体重详情页面展示用户的体重数据,包括当前体重、目标体重、BMI、趋势图表和历史记录。这是一个典型的数据展示页面,会用到 fl_chart 库来绘制折线图。

这篇文章会讲解如何组织数据展示页面的结构,以及如何使用图表库。


页面结构

详情页面分为三个部分:当前数据卡片、趋势图表、历史记录列表。

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFFFAFAFC),
      appBar: AppBar(
        backgroundColor: Colors.transparent,
        leading: IconButton(
          icon: Icon(Icons.arrow_back_ios_rounded, size: 20.w), 
          onPressed: () => Get.back()
        ),
        title: Text('体重详情', style: TextStyle(
          fontSize: 17.sp, 
          fontWeight: FontWeight.w600
        )),
        centerTitle: true,
      ),
      body: SingleChildScrollView(
        padding: EdgeInsets.all(20.w),
        child: Column(
          children: [
            _buildCurrentWeight(),
            SizedBox(height: 20.h),
            _buildChart(),
            SizedBox(height: 20.h),
            _buildHistory(),
          ],
        ),
      ),
    );
  }

详情页面用返回箭头而不是关闭图标,因为这是一个查看页面而不是编辑页面。arrow_back_ios_rounded 是 iOS 风格的返回箭头。


当前体重卡片

用渐变背景的卡片突出显示当前体重和相关指标。

  Widget _buildCurrentWeight() {
    return Container(
      width: double.infinity,
      padding: EdgeInsets.all(24.w),
      decoration: BoxDecoration(
        gradient: const LinearGradient(
          colors: [Color(0xFFFF6B6B), Color(0xFFFF8E8E)]
        ),
        borderRadius: BorderRadius.circular(24.r),
      ),
      child: Column(
        children: [
          Text('当前体重', style: TextStyle(
            fontSize: 13.sp, 
            color: Colors.white70
          )),
          SizedBox(height: 8.h),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.end,
            children: [
              Text('65.5', style: TextStyle(
                fontSize: 48.sp, 
                fontWeight: FontWeight.w700, 
                color: Colors.white
              )),
              Padding(
                padding: EdgeInsets.only(bottom: 8.h),
                child: Text(' kg', style: TextStyle(
                  fontSize: 18.sp, 
                  color: Colors.white70
                )),
              ),
            ],
          ),

红色渐变是体重在整个App中的主题色。48sp 的大字号让体重数值成为视觉焦点。


辅助指标

在体重下方显示目标、BMI、本周变化三个辅助指标。

          SizedBox(height: 12.h),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              _buildStatItem('目标', '63 kg'),
              Container(
                width: 1, 
                height: 24.h, 
                color: Colors.white24, 
                margin: EdgeInsets.symmetric(horizontal: 24.w)
              ),
              _buildStatItem('BMI', '21.4'),
              Container(
                width: 1, 
                height: 24.h, 
                color: Colors.white24, 
                margin: EdgeInsets.symmetric(horizontal: 24.w)
              ),
              _buildStatItem('本周', '-0.3 kg'),
            ],
          ),
        ],
      ),
    );
  }

  Widget _buildStatItem(String label, String value) {
    return Column(
      children: [
        Text(value, style: TextStyle(
          fontSize: 15.sp, 
          fontWeight: FontWeight.w600, 
          color: Colors.white
        )),
        SizedBox(height: 2.h),
        Text(label, style: TextStyle(
          fontSize: 11.sp, 
          color: Colors.white60
        )),
      ],
    );
  }

用竖线分隔三个指标,Colors.white24 是 24% 透明度的白色,在渐变背景上不会太突兀。

BMI(Body Mass Index)= 体重(kg) / 身高(m)²,21.4 属于正常范围(18.5-24.9)。


趋势图表

fl_chart 库绘制体重变化的折线图。

  Widget _buildChart() {
    return Container(
      padding: EdgeInsets.all(20.w),
      decoration: BoxDecoration(
        color: Colors.white, 
        borderRadius: BorderRadius.circular(20.r)
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('趋势图', style: TextStyle(
            fontSize: 16.sp, 
            fontWeight: FontWeight.w600, 
            color: const Color(0xFF1A1A2E)
          )),
          SizedBox(height: 20.h),
          SizedBox(
            height: 180.h,
            child: LineChart(
              LineChartData(
                gridData: FlGridData(show: false),
                titlesData: FlTitlesData(
                  rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
                  topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
                  leftTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
                  bottomTitles: AxisTitles(
                    sideTitles: SideTitles(
                      showTitles: true, 
                      getTitlesWidget: (v, m) => Padding(
                        padding: EdgeInsets.only(top: 8.h), 
                        child: Text(
                          ['1/5', '1/6', '1/7', '1/8', '1/9', '1/10', '1/11'][v.toInt() % 7], 
                          style: TextStyle(fontSize: 10.sp, color: Colors.grey[400])
                        )
                      )
                    )
                  ),
                ),
                borderData: FlBorderData(show: false),
                lineBarsData: [
                  LineChartBarData(
                    spots: const [
                      FlSpot(0, 66.2), FlSpot(1, 65.8), FlSpot(2, 66.0), 
                      FlSpot(3, 65.5), FlSpot(4, 65.7), FlSpot(5, 65.3), FlSpot(6, 65.5)
                    ],
                    isCurved: true,
                    color: const Color(0xFFFF6B6B),
                    barWidth: 3,
                    dotData: FlDotData(
                      show: true, 
                      getDotPainter: (s, p, b, i) => FlDotCirclePainter(
                        radius: 4, 
                        color: Colors.white, 
                        strokeWidth: 2, 
                        strokeColor: const Color(0xFFFF6B6B)
                      )
                    ),
                    belowBarData: BarAreaData(
                      show: true, 
                      color: const Color(0xFFFF6B6B).withOpacity(0.1)
                    ),
                  ),
                ],
                minY: 64, maxY: 68,
              ),
            ),
          ),
        ],
      ),
    );
  }

FlGridData(show: false) 隐藏网格线,让图表更简洁。isCurved: true 让折线变成平滑的曲线。

belowBarData 在曲线下方填充一层淡色,增加视觉层次感。minYmaxY 设置 Y 轴范围,让数据变化更明显。


历史记录列表

显示最近几天的体重记录,包括日期、时间、数值和变化量。

  Widget _buildHistory() {
    final history = [
      {'date': '1月11日', 'time': '08:32', 'value': '65.5 kg', 'change': '-0.2'},
      {'date': '1月10日', 'time': '08:15', 'value': '65.7 kg', 'change': '+0.4'},
      {'date': '1月9日', 'time': '08:20', 'value': '65.3 kg', 'change': '-0.2'},
      {'date': '1月8日', 'time': '08:10', 'value': '65.5 kg', 'change': '-0.5'},
      {'date': '1月7日', 'time': '08:25', 'value': '66.0 kg', 'change': '+0.2'},
    ];

    return Container(
      padding: EdgeInsets.all(20.w),
      decoration: BoxDecoration(
        color: Colors.white, 
        borderRadius: BorderRadius.circular(20.r)
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('历史记录', style: TextStyle(
            fontSize: 16.sp, 
            fontWeight: FontWeight.w600, 
            color: const Color(0xFF1A1A2E)
          )),
          SizedBox(height: 16.h),
          ...history.map((item) => Padding(
            padding: EdgeInsets.only(bottom: 14.h),
            child: Row(
              children: [
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(item['date']!, style: TextStyle(
                        fontSize: 14.sp, 
                        fontWeight: FontWeight.w500, 
                        color: const Color(0xFF1A1A2E)
                      )),
                      SizedBox(height: 2.h),
                      Text(item['time']!, style: TextStyle(
                        fontSize: 12.sp, 
                        color: Colors.grey[400]
                      )),
                    ],
                  ),
                ),
                Text(item['value']!, style: TextStyle(
                  fontSize: 15.sp, 
                  fontWeight: FontWeight.w600, 
                  color: const Color(0xFF1A1A2E)
                )),
                SizedBox(width: 12.w),
                Container(
                  padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 2.h),
                  decoration: BoxDecoration(
                    color: item['change']!.startsWith('-') 
                      ? const Color(0xFF00C9A7).withOpacity(0.12) 
                      : const Color(0xFFFF6B6B).withOpacity(0.12),
                    borderRadius: BorderRadius.circular(6.r),
                  ),
                  child: Text(item['change']!, style: TextStyle(
                    fontSize: 11.sp, 
                    color: item['change']!.startsWith('-') 
                      ? const Color(0xFF00C9A7) 
                      : const Color(0xFFFF6B6B)
                  )),
                ),
              ],
            ),
          )),
        ],
      ),
    );
  }
}

变化量用颜色区分:减少用绿色(好事),增加用红色(需要注意)。这个颜色逻辑假设用户的目标是减重,如果是增重目标,颜色应该反过来。

...history.map() 用展开运算符把 Iterable<Widget> 展开成多个 Widget,放入 Columnchildren 列表中。


小结

体重详情页面的特点:

  • 渐变卡片突出显示当前体重
  • 辅助指标(目标、BMI、本周变化)提供更多信息
  • 折线图展示趋势变化
  • 历史记录列表显示详细数据

这种"卡片 + 图表 + 列表"的结构在数据展示页面中很常见,可以复用到其他详情页面。

下一篇会讲血压详情页面,血压需要同时展示收缩压和舒张压两条曲线。


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

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐