在这里插入图片描述

猫咪的健康记录需要一个地方集中展示和管理。今天我们来实现健康记录列表页面,支持按类型筛选、查看详情、添加新记录等功能。


功能需求

健康记录列表需要实现:

  • 展示所有健康记录
  • 支持按类型筛选(疫苗、驱虫、体检等)
  • 显示记录的关键信息
  • 空状态友好提示
  • 快速添加新记录

这些功能组合起来,就是一个完整的健康记录管理页面。


依赖引入

首先导入需要的包:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:intl/intl.dart';
import '../../providers/health_provider.dart';
import '../../models/health_record.dart';
import 'add_health_record_screen.dart';

Provider用于状态管理,监听数据变化自动刷新UI。
intl包处理日期格式化显示。


有状态组件

列表页面需要维护筛选状态:

class HealthRecordListScreen extends StatefulWidget {
  final String catId;

  const HealthRecordListScreen({super.key, required this.catId});

  
  State<HealthRecordListScreen> createState() => _HealthRecordListScreenState();
}

catId标识是哪只猫咪的健康记录。
StatefulWidget用于管理筛选类型的状态。


筛选状态

State类中定义筛选变量:

class _HealthRecordListScreenState extends State<HealthRecordListScreen> {
  HealthRecordType? _filterType;

_filterType为null时显示全部记录。
选择具体类型后只显示该类型的记录。


页面结构

build方法构建整体布局:

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('健康记录'),
        actions: [
          PopupMenuButton<HealthRecordType?>(
            icon: const Icon(Icons.filter_list),
            onSelected: (type) => setState(() => _filterType = type),
            itemBuilder: (context) => [
              const PopupMenuItem(value: null, child: Text('全部')),
              ...HealthRecordType.values.map((type) => PopupMenuItem(
                value: type,
                child: Text(_getTypeString(type)),
              )),
            ],
          ),
        ],
      ),

AppBar的actions放置筛选按钮。
PopupMenuButton点击弹出筛选菜单。


数据监听

Consumer监听Provider数据变化:

      body: Consumer<HealthProvider>(
        builder: (context, provider, child) {
          var records = provider.getRecordsForCat(widget.catId);
          if (_filterType != null) {
            records = records.where((r) => r.type == _filterType).toList();
          }

getRecordsForCat获取指定猫咪的所有记录。
如果设置了筛选类型,用where方法过滤。


空状态处理

没有记录时显示引导界面:

          if (records.isEmpty) {
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(Icons.medical_services, size: 80.sp, color: Colors.grey[300]),
                  SizedBox(height: 16.h),
                  Text('暂无健康记录', style: TextStyle(color: Colors.grey[600])),
                ],
              ),
            );
          }

大图标让页面不会太空。
灰色调表示空状态。


列表展示

有数据时用ListView展示:

          return ListView.builder(
            padding: EdgeInsets.all(16.w),
            itemCount: records.length,
            itemBuilder: (context, index) => _buildRecordCard(records[index]),
          );
        },
      ),

ListView.builder按需构建列表项,性能更好。
padding给列表留出边距。


悬浮添加按钮

页面底部的FAB:

      floatingActionButton: FloatingActionButton(
        onPressed: () => Navigator.push(context, MaterialPageRoute(
          builder: (_) => AddHealthRecordScreen(catId: widget.catId),
        )),
        backgroundColor: Colors.orange,
        child: const Icon(Icons.add),
      ),
    );
  }

FloatingActionButton是Material Design的标准添加按钮。
点击跳转到添加健康记录页面。


记录卡片组件

构建单条记录的卡片:

  Widget _buildRecordCard(HealthRecord record) {
    return Card(
      margin: EdgeInsets.only(bottom: 12.h),
      child: ListTile(
        leading: CircleAvatar(
          backgroundColor: _getTypeColor(record.type).withOpacity(0.1),
          child: Icon(_getTypeIcon(record.type), color: _getTypeColor(record.type)),
        ),
        title: Text(record.title),

Card提供阴影和圆角效果。
CircleAvatar显示类型对应的图标。

副标题显示详细信息:

        subtitle: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('${record.typeString} · ${record.hospital ?? ""}'),
            Text(DateFormat('yyyy-MM-dd').format(record.date)),
          ],
        ),

显示记录类型、医院和日期。
医院为null时显示空字符串。

费用显示:

        trailing: record.cost != null
            ? Text(${record.cost!.toStringAsFixed(0)}', style: TextStyle(color: Colors.orange))
            : null,
      ),
    );
  }

有费用时显示金额,没有则不显示。
橙色突出显示费用信息。


类型转换方法

枚举转中文:

  String _getTypeString(HealthRecordType type) {
    switch (type) {
      case HealthRecordType.vaccination: return '疫苗接种';
      case HealthRecordType.deworming: return '驱虫';
      case HealthRecordType.checkup: return '体检';
      case HealthRecordType.surgery: return '手术';
      case HealthRecordType.medication: return '用药';
      case HealthRecordType.other: return '其他';
    }
  }

switch语句处理每种类型。
六种类型覆盖常见的健康记录场景。


类型颜色映射

不同类型用不同颜色区分:

  Color _getTypeColor(HealthRecordType type) {
    switch (type) {
      case HealthRecordType.vaccination: return Colors.blue;
      case HealthRecordType.deworming: return Colors.green;
      case HealthRecordType.checkup: return Colors.purple;
      case HealthRecordType.surgery: return Colors.red;
      case HealthRecordType.medication: return Colors.orange;
      case HealthRecordType.other: return Colors.grey;
    }
  }

疫苗用蓝色,驱虫用绿色,手术用红色。
颜色区分让用户一眼就能识别类型。


类型图标映射

不同类型用不同图标:

  IconData _getTypeIcon(HealthRecordType type) {
    switch (type) {
      case HealthRecordType.vaccination: return Icons.vaccines;
      case HealthRecordType.deworming: return Icons.bug_report;
      case HealthRecordType.checkup: return Icons.medical_services;
      case HealthRecordType.surgery: return Icons.local_hospital;
      case HealthRecordType.medication: return Icons.medication;
      case HealthRecordType.other: return Icons.note;
    }
  }
}

图标让类型更直观。
Material Icons提供了丰富的医疗相关图标。


PopupMenuButton详解

弹出菜单按钮的使用:

PopupMenuButton<HealthRecordType?>(
  icon: const Icon(Icons.filter_list),
  onSelected: (type) => setState(() => _filterType = type),
  itemBuilder: (context) => [
    const PopupMenuItem(value: null, child: Text('全部')),
    ...HealthRecordType.values.map((type) => PopupMenuItem(
      value: type,
      child: Text(_getTypeString(type)),
    )),
  ],
)

icon设置按钮图标。
itemBuilder返回菜单项列表。


筛选逻辑

where方法过滤列表:

if (_filterType != null) {
  records = records.where((r) => r.type == _filterType).toList();
}

where返回符合条件的元素。
toList转换为List类型。


Consumer使用

监听Provider数据变化:

Consumer<HealthProvider>(
  builder: (context, provider, child) {
    var records = provider.getRecordsForCat(widget.catId);
    // 使用records构建UI
  },
)

builder在数据变化时重新执行。
provider参数可以调用Provider的方法。


CircleAvatar样式

圆形头像的自定义:

CircleAvatar(
  backgroundColor: _getTypeColor(record.type).withOpacity(0.1),
  child: Icon(_getTypeIcon(record.type), color: _getTypeColor(record.type)),
)

背景色用10%透明度,不会太抢眼。
图标颜色与背景色呼应。


ListTile布局

列表项的标准布局:

ListTile(
  leading: CircleAvatar(...),  // 左侧图标
  title: Text(record.title),   // 主标题
  subtitle: Column(...),       // 副标题
  trailing: Text(...),         // 右侧内容
)

四个位置可以放置不同内容。
自动处理间距和对齐。


条件渲染

根据数据是否存在决定显示:

trailing: record.cost != null
    ? Text(${record.cost!.toStringAsFixed(0)}', ...)
    : null,

有费用时显示金额。
null的trailing不占用空间。


日期格式化

格式化日期显示:

DateFormat('yyyy-MM-dd').format(record.date)

输出类似"2024-03-15"的格式。
intl包提供了丰富的格式化选项。


空状态设计

好的空状态应该包含:

Center(
  child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      Icon(Icons.medical_services, size: 80.sp, color: Colors.grey[300]),
      SizedBox(height: 16.h),
      Text('暂无健康记录', style: TextStyle(color: Colors.grey[600])),
    ],
  ),
)

大图标让页面不会太空。
文字说明当前状态。


小结

健康记录列表涉及的知识点:

  • PopupMenuButton筛选菜单
  • Consumer监听数据变化
  • 条件渲染和空状态处理
  • 颜色和图标的映射

这些技巧在其他列表页面也能复用。


欢迎加入OpenHarmony跨平台开发社区,一起交流Flutter开发经验:

https://openharmonycrossplatform.csdn.net

Logo

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

更多推荐