在这里插入图片描述

在家庭健康管理中,血压监测是非常重要的一环,特别是对于中老年人群。定期记录血压数据不仅能帮助我们了解身体状况,还能在就医时为医生提供重要的参考依据。本文将详细介绍如何在Flutter for OpenHarmony应用中实现一个功能完善的血压记录模块,包括数据录入、趋势图表展示、健康状态评估等核心功能。

血压记录功能看似简单,实则需要考虑很多细节。比如如何让用户快速录入数据、如何直观展示血压变化趋势、如何根据医学标准智能判断健康状态等。这些都需要我们在设计和开发时仔细思考。

整体设计思路

血压记录模块的核心在于数据的可视化和健康状态的智能判断。我们需要记录收缩压、舒张压和心率三个关键指标,并通过图表直观展示血压变化趋势。同时,系统会根据医学标准自动判断血压状态,给用户提供健康参考。

整个页面采用卡片式布局,顶部是成员选择器,中间展示趋势图表和最新记录,底部列出历史数据。这种设计让用户能够快速了解血压变化情况,发现异常及时就医。卡片之间使用适当的间距分隔,视觉上清晰舒适。

页面结构搭建

首先看页面的整体结构,我们使用Consumer2同时监听家庭成员和健康数据两个Provider:


Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: const Text('血压记录'),
    ),
    body: Consumer2<FamilyProvider, HealthProvider>(
      builder: (context, familyProvider, healthProvider, child) {
        if (_selectedMemberId == null && familyProvider.members.isNotEmpty) {
          _selectedMemberId = familyProvider.members.first.id;
        }

        final records = _selectedMemberId != null
            ? healthProvider.getRecordsByMemberAndType(_selectedMemberId!, 'blood_pressure')
            : <HealthRecord>[];

        return SingleChildScrollView(
          padding: EdgeInsets.all(16.w),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              _buildMemberSelector(familyProvider),
              SizedBox(height: 16.h),
              if (records.isNotEmpty) _buildChart(records),
              SizedBox(height: 16.h),
              _buildLatestRecord(records),
              SizedBox(height: 16.h),
              _buildRecordList(records),
            ],
          ),
        );
      },
    ),
    floatingActionButton: FloatingActionButton(
      onPressed: () => _showAddDialog(context),
      backgroundColor: const Color(0xFF00897B),
      child: const Icon(Icons.add),
    ),
  );
}

这里使用Consumer2可以同时监听两个数据源的变化,当家庭成员或健康记录发生变化时,页面会自动刷新。通过getRecordsByMemberAndType方法筛选出当前选中成员的血压记录,确保数据的准确性。

页面使用SingleChildScrollView包裹,让内容可以滚动,适应不同屏幕尺寸。各个模块通过SizedBox进行间隔,视觉上更加舒适。右下角的浮动按钮用于添加新记录,使用主题色背景,与整体风格保持一致。

成员选择器实现

成员选择器让用户可以切换查看不同家庭成员的血压记录:

Widget _buildMemberSelector(FamilyProvider provider) {
  return Container(
    padding: EdgeInsets.symmetric(horizontal: 12.w),
    decoration: BoxDecoration(
      color: Colors.white,
      borderRadius: BorderRadius.circular(8.r),
      border: Border.all(color: Colors.grey[300]!),
    ),
    child: DropdownButtonHideUnderline(
      child: DropdownButton<String>(
        value: _selectedMemberId,
        isExpanded: true,
        items: provider.members.map((m) {
          return DropdownMenuItem(value: m.id, child: Text(m.name));
        }).toList(),
        onChanged: (value) {
          setState(() {
            _selectedMemberId = value;
          });
        },
      ),
    ),
  );
}

下拉选择器采用白色背景和圆角边框设计,视觉上更加清爽。使用DropdownButtonHideUnderline隐藏默认下划线,让整体样式更统一。当用户切换成员时,通过setState触发页面重建,自动加载对应成员的血压数据。

这种设计的好处是用户可以方便地在不同家庭成员之间切换,查看各自的血压记录。特别是在有老人的家庭中,这个功能非常实用。选择器使用isExpanded属性让下拉框占满容器宽度,避免文字被截断。

趋势图表展示

血压趋势图使用fl_chart库实现,可以同时展示收缩压和舒张压的变化曲线:

Widget _buildChart(List<HealthRecord> records) {
  final chartRecords = records.take(7).toList().reversed.toList();

  return Container(
    height: 200.h,
    padding: EdgeInsets.all(16.w),
    decoration: BoxDecoration(
      color: Colors.white,
      borderRadius: BorderRadius.circular(12.r),
      boxShadow: [
        BoxShadow(
          color: Colors.black.withOpacity(0.05),
          blurRadius: 10,
          offset: const Offset(0, 2),
        ),
      ],
    ),
    child: LineChart(
      LineChartData(
        gridData: FlGridData(show: false),
        titlesData: FlTitlesData(show: false),
        borderData: FlBorderData(show: false),
        lineBarsData: [
          LineChartBarData(
            spots: chartRecords.asMap().entries.map((e) {
              return FlSpot(e.key.toDouble(), (e.value.data['systolic'] as int).toDouble());
            }).toList(),
            isCurved: true,
            color: Colors.red,
            barWidth: 2,
            dotData: FlDotData(show: true),
          ),
          LineChartBarData(
            spots: chartRecords.asMap().entries.map((e) {
              return FlSpot(e.key.toDouble(), (e.value.data['diastolic'] as int).toDouble());
            }).toList(),
            isCurved: true,
            color: Colors.blue,
            barWidth: 2,
            dotData: FlDotData(show: true),
          ),
        ],
      ),
    ),
  );
}

图表取最近7天的数据进行展示,红色曲线代表收缩压,蓝色曲线代表舒张压。使用isCurved让曲线更加平滑,视觉效果更好。通过asMap().entries可以同时获取索引和数据,方便构建图表坐标点。

这里有个小技巧,我们先用take(7)取最近7条记录,然后用reversed反转顺序,这样图表就能按时间顺序从左到右展示。图表隐藏了网格线、坐标轴和边框,让界面更加简洁,突出数据本身。每个数据点都显示圆点标记,方便用户查看具体数值。

最新记录卡片

最新记录卡片是页面的核心,展示最近一次测量的详细数据和健康状态:

Widget _buildLatestRecord(List<HealthRecord> records) {
  if (records.isEmpty) {
    return Container(
      padding: EdgeInsets.all(20.w),
      decoration: BoxDecoration(
        color: Colors.grey[100],
        borderRadius: BorderRadius.circular(12.r),
      ),
      child: Center(
        child: Text('暂无血压记录', style: TextStyle(color: Colors.grey[500])),
      ),
    );
  }

  final latest = records.first;
  final systolic = latest.data['systolic'] as int;
  final diastolic = latest.data['diastolic'] as int;
  final heartRate = latest.data['heartRate'] as int;

  String status = '正常';
  Color statusColor = Colors.green;
  if (systolic >= 140 || diastolic >= 90) {
    status = '偏高';
    statusColor = Colors.red;
  } else if (systolic < 90 || diastolic < 60) {
    status = '偏低';
    statusColor = Colors.orange;
  }

  return Container(
    padding: EdgeInsets.all(16.w),
    decoration: BoxDecoration(
      color: Colors.white,
      borderRadius: BorderRadius.circular(12.r),
      boxShadow: [
        BoxShadow(
          color: Colors.black.withOpacity(0.05),
          blurRadius: 10,
          offset: const Offset(0, 2),
        ),
      ],
    ),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text('最新记录', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold)),
            Container(
              padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
              decoration: BoxDecoration(
                color: statusColor.withOpacity(0.1),
                borderRadius: BorderRadius.circular(10.r),
              ),
              child: Text(status, style: TextStyle(color: statusColor, fontSize: 12.sp)),
            ),
          ],
        ),
        SizedBox(height: 16.h),
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: [
            _buildValueItem('收缩压', '$systolic', 'mmHg', Colors.red),
            _buildValueItem('舒张压', '$diastolic', 'mmHg', Colors.blue),
            _buildValueItem('心率', '$heartRate', 'bpm', Colors.purple),
          ],
        ),
        SizedBox(height: 8.h),
        Text(
          DateFormat('yyyy-MM-dd HH:mm').format(latest.recordDate),
          style: TextStyle(fontSize: 12.sp, color: Colors.grey[500]),
        ),
      ],
    ),
  );
}

这段代码实现了智能的健康状态判断逻辑。根据医学标准,收缩压≥140或舒张压≥90判定为偏高,收缩压<90或舒张压<60判定为偏低。状态标签使用不同颜色的背景,让用户一眼就能看出血压是否正常。

卡片顶部显示"最新记录"标题和健康状态标签,中间是三个关键指标的数值展示,底部是记录时间。这种布局让信息层次分明,用户可以快速获取关键信息。如果没有记录,则显示友好的空状态提示,引导用户添加第一条记录。

数值展示组件

为了让三个关键指标展示更加统一,我们封装了一个数值展示组件:

Widget _buildValueItem(String label, String value, String unit, Color color) {
  return Column(
    children: [
      Text(label, style: TextStyle(fontSize: 12.sp, color: Colors.grey[600])),
      SizedBox(height: 4.h),
      Text(value, style: TextStyle(fontSize: 24.sp, fontWeight: FontWeight.bold, color: color)),
      Text(unit, style: TextStyle(fontSize: 10.sp, color: Colors.grey[500])),
    ],
  );
}

这个组件采用垂直布局,标签在上、数值居中、单位在下。数值使用大号粗体字体突出显示,并根据类型使用不同颜色:收缩压用红色、舒张压用蓝色、心率用紫色,让数据更加直观易读。

这种设计的好处是用户可以快速识别不同的指标,颜色编码让信息传达更加高效。标签和单位使用灰色小号字体,不抢夺数值的视觉焦点。整个组件的高度和宽度都是自适应的,可以很好地适应不同屏幕尺寸。

历史记录列表

历史记录列表展示最近10条血压测量数据:

Widget _buildRecordList(List<HealthRecord> records) {
  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Text('历史记录', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold)),
      SizedBox(height: 8.h),
      ...records.take(10).map((record) {
        return Container(
          margin: EdgeInsets.only(bottom: 8.h),
          padding: EdgeInsets.all(12.w),
          decoration: BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.circular(8.r),
          ),
          child: Row(
            children: [
              Text(
                DateFormat('MM-dd').format(record.recordDate),
                style: TextStyle(fontSize: 14.sp, color: Colors.grey[600]),
              ),
              SizedBox(width: 16.w),
              Text(
                '${record.data['systolic']}/${record.data['diastolic']}',
                style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w500),
              ),
              const Spacer(),
              Text(
                '${record.data['heartRate']} bpm',
                style: TextStyle(fontSize: 14.sp, color: Colors.grey[600]),
              ),
            ],
          ),
        );
      }).toList(),
    ],
  );
}

每条记录采用横向布局,左侧显示日期,中间显示血压值(收缩压/舒张压),右侧显示心率。使用take(10)限制显示数量,避免列表过长影响性能。日期格式简化为月-日,让界面更加简洁。

历史记录列表让用户可以快速浏览过往的血压数据,发现变化趋势。每条记录使用白色背景和圆角设计,与整体风格保持一致。记录之间有适当的间距,避免视觉上过于拥挤。血压值使用稍大的字体和中等粗细,作为每条记录的视觉焦点。

添加记录对话框

点击浮动按钮可以弹出添加记录的对话框:

void _showAddDialog(BuildContext context) {
  final systolicController = TextEditingController();
  final diastolicController = TextEditingController();
  final heartRateController = TextEditingController();

  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('添加血压记录'),
      content: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          TextField(
            controller: systolicController,
            keyboardType: TextInputType.number,
            decoration: const InputDecoration(labelText: '收缩压 (mmHg)'),
          ),
          TextField(
            controller: diastolicController,
            keyboardType: TextInputType.number,
            decoration: const InputDecoration(labelText: '舒张压 (mmHg)'),
          ),
          TextField(
            controller: heartRateController,
            keyboardType: TextInputType.number,
            decoration: const InputDecoration(labelText: '心率 (bpm)'),
          ),
        ],
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('取消'),
        ),
        TextButton(
          onPressed: () {
            if (_selectedMemberId != null) {
              final member = context.read<FamilyProvider>().getMemberById(_selectedMemberId!);
              if (member != null) {
                final record = HealthRecord(
                  id: const Uuid().v4(),
                  memberId: member.id,
                  memberName: member.name,
                  recordDate: DateTime.now(),
                  type: 'blood_pressure',
                  data: {
                    'systolic': int.tryParse(systolicController.text) ?? 0,
                    'diastolic': int.tryParse(diastolicController.text) ?? 0,
                    'heartRate': int.tryParse(heartRateController.text) ?? 0,
                  },
                );
                context.read<HealthProvider>().addRecord(record);
              }
            }
            Navigator.pop(context);
            Get.snackbar('成功', '血压记录已添加', snackPosition: SnackPosition.BOTTOM);
          },
          child: const Text('保存'),
        ),
      ],
    ),
  );
}

对话框包含三个输入框,分别用于输入收缩压、舒张压和心率。使用TextInputType.number限制只能输入数字,提升用户体验。保存时使用int.tryParse进行安全的类型转换,避免输入非法字符导致崩溃。

这个对话框的设计非常简洁,只包含必要的输入项,降低用户的操作负担。输入框使用标准的Material Design样式,带有浮动标签,用户体验良好。保存成功后会显示提示信息,让用户知道操作已完成。使用Uuid生成唯一ID,确保每条记录都有唯一标识。

总结

血压记录功能是健康管理模块的重要组成部分。通过图表可视化、智能状态判断、多成员支持等设计,让用户能够方便地记录和追踪血压变化。这种数据驱动的健康管理方式,可以帮助用户及时发现异常,做好健康预防工作。


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

Logo

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

更多推荐