在这里插入图片描述

记录页面是口腔护理App的核心功能之一,用户可以在这里查看自己的护理历史,包括刷牙、漱口、牙线使用等各类记录。这个页面整合了日历视图和记录列表,让用户能够直观地了解自己的口腔护理习惯。

为什么需要记录功能

养成良好的口腔护理习惯需要持续的坚持和反馈。通过记录功能,用户可以清楚地看到自己每天的护理情况,哪些天坚持得好,哪些天有所懈怠。这种可视化的反馈机制能够有效激励用户保持良好习惯。记录功能还能帮助用户在就医时向医生展示自己的护理历史,便于医生做出更准确的诊断。

引入必要的依赖

import 'package:flutter/material.dart';

Flutter核心库提供了构建UI所需的所有基础组件。

Material Design风格的控件让应用看起来更加专业。

import 'package:provider/provider.dart';

Provider是Flutter官方推荐的状态管理方案,轻量且易于使用。

它让我们能够在整个应用中共享护理记录数据,任何地方添加的记录都能实时同步到这个页面。

import 'package:table_calendar/table_calendar.dart';

table_calendar是一个功能强大的日历组件库。

支持周视图、月视图切换,还能在日期上显示事件标记,非常适合记录类应用。

import 'package:intl/intl.dart';

intl库用于日期时间的格式化显示。

比如把DateTime对象格式化成"08:30"这样的字符串,方便用户阅读。

import '../../providers/app_provider.dart';

AppProvider是我们自定义的状态管理类。

它存储了所有护理记录数据,包括刷牙、漱口、牙线等各类记录。

import '../pages/brush_history_page.dart';
import '../pages/mouthwash_page.dart';
import '../pages/floss_page.dart';

这些是各个记录详情页面的导入。

点击分类卡片后会跳转到对应页面查看详细记录。

import '../pages/diet_record_page.dart';
import '../pages/oral_check_page.dart';
import '../pages/oral_issue_page.dart';

饮食记录、检查记录、口腔问题页面的导入。

这些页面提供了更专业的记录管理功能。

使用StatefulWidget管理状态

记录页面需要管理日历的选中状态和显示格式,所以使用StatefulWidget。

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

  
  State<RecordTab> createState() => _RecordTabState();
}

StatefulWidget适合需要维护本地UI状态的场景。

createState方法返回State对象,Flutter会在需要时调用它。

class _RecordTabState extends State<RecordTab> {
  DateTime _focusedDay = DateTime.now();

_focusedDay表示日历当前聚焦的日期。

初始化为今天,用户打开页面时默认显示当前月份。

  DateTime? _selectedDay;

_selectedDay是用户点击选中的日期。

使用可空类型是因为初始状态下可能没有选中任何日期。

  CalendarFormat _calendarFormat = CalendarFormat.week;

_calendarFormat控制日历显示为周视图还是月视图。

默认显示周视图,更紧凑,适合日常查看。

页面整体布局结构

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('护理记录'),
      ),

Scaffold提供了标准的Material Design页面结构。

AppBar显示页面标题"护理记录",让用户知道当前所在位置。

      body: Consumer<AppProvider>(
        builder: (context, provider, _) {

Consumer监听AppProvider的数据变化。

当有新记录添加时,builder会自动重新执行,页面随之刷新。

          return SingleChildScrollView(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,

SingleChildScrollView让整个页面可以滚动。

Column垂直排列各个模块,crossAxisAlignment设为start让内容左对齐。

              children: [
                _buildCalendar(provider),

日历组件放在最上方,是这个页面的核心视觉元素。

传入provider是为了获取记录数据,在日历上标记有记录的日期。

                Padding(
                  padding: const EdgeInsets.all(16),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [

下方内容用Padding包裹,添加16像素的内边距。

嵌套的Column用于垂直排列护理记录分类和今日记录。

                      const Text('护理记录', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                      const SizedBox(height: 12),
                      _buildRecordCategories(context),

"护理记录"标题使用18像素加粗字体。

下方是分类网格,展示六种护理记录类型。

                      const SizedBox(height: 20),
                      const Text('今日记录', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                      const SizedBox(height: 12),
                      _buildTodayRecords(provider),
                    ],
                  ),
                ),
              ],
            ),
          );
        },
      ),
    );
  }

"今日记录"区域展示当天的所有护理记录。

模块之间用SizedBox添加间距,让布局更有层次感。

日历组件的实现细节

日历是记录页面的亮点功能,用户可以通过日历查看历史记录。

  Widget _buildCalendar(AppProvider provider) {
    return Container(
      margin: const EdgeInsets.all(16),

日历外层用Container包裹,四周添加16像素外边距。

这样日历不会紧贴屏幕边缘,视觉上更舒适。

      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(16),
        boxShadow: [BoxShadow(color: Colors.grey.shade200, blurRadius: 10)],
      ),

白色背景让日历在灰色页面背景上更加突出。

16像素圆角和浅色阴影形成卡片效果,符合现代UI设计趋势。

      child: TableCalendar(
        firstDay: DateTime.utc(2020, 1, 1),
        lastDay: DateTime.utc(2030, 12, 31),

TableCalendar需要设置日期范围。

firstDay和lastDay定义了用户可以浏览的日期边界,这里设置了10年的范围。

        focusedDay: _focusedDay,
        calendarFormat: _calendarFormat,

focusedDay决定日历当前显示哪个月份。

calendarFormat控制显示周视图还是月视图。

        selectedDayPredicate: (day) => isSameDay(_selectedDay, day),

selectedDayPredicate是一个判断函数。

isSameDay是table_calendar提供的工具函数,用于比较两个日期是否是同一天。

        onDaySelected: (selectedDay, focusedDay) {
          setState(() {
            _selectedDay = selectedDay;
            _focusedDay = focusedDay;
          });
        },

用户点击某天时触发onDaySelected回调。

通过setState更新状态,日历会重新渲染显示选中效果。

        onFormatChanged: (format) {
          setState(() {
            _calendarFormat = format;
          });
        },

用户点击格式切换按钮时触发这个回调。

可以在周视图和月视图之间切换,满足不同的查看需求。

日历样式配置

        calendarStyle: const CalendarStyle(
          selectedDecoration: BoxDecoration(
            color: Color(0xFF26A69A),
            shape: BoxShape.circle,
          ),

selectedDecoration定义选中日期的样式。

使用主题绿色圆形背景,与整体设计风格保持一致。

          todayDecoration: BoxDecoration(
            color: Color(0xFF80CBC4),
            shape: BoxShape.circle,
          ),
        ),

todayDecoration定义今天日期的样式。

使用稍浅的绿色,让用户能区分"今天"和"选中的日期"。

        headerStyle: const HeaderStyle(
          formatButtonVisible: true,
          titleCentered: true,
        ),

headerStyle配置日历头部样式。

formatButtonVisible显示格式切换按钮,titleCentered让月份标题居中显示。

日历事件标记功能

在日历上标记有记录的日期,让用户一眼看出哪些天有护理记录。

        eventLoader: (day) {
          final count = provider.brushRecords.where((r) =>
            r.dateTime.year == day.year &&
            r.dateTime.month == day.month &&
            r.dateTime.day == day.day
          ).length;
          return List.generate(count, (i) => i);
        },

eventLoader为每个日期返回事件列表。

这里统计当天的刷牙记录数量,返回对应长度的列表作为事件标记的依据。

        calendarBuilders: CalendarBuilders(
          markerBuilder: (context, date, events) {
            if (events.isNotEmpty) {
              return Positioned(
                bottom: 1,

calendarBuilders允许自定义日历各部分的渲染方式。

markerBuilder用于自定义事件标记的显示,Positioned定位标记在日期下方。

                child: Container(
                  width: 6,
                  height: 6,
                  decoration: const BoxDecoration(
                    color: Color(0xFF26A69A),
                    shape: BoxShape.circle,
                  ),
                ),
              );
            }
            return null;
          },
        ),
      ),
    );
  }

有记录的日期下方显示一个6像素的小绿点。

简洁明了,不会干扰日期数字的显示。

记录分类网格布局

六种护理记录分类用网格布局展示,方便用户快速进入对应的记录页面。

  Widget _buildRecordCategories(BuildContext context) {
    final categories = [
      {'icon': Icons.brush, 'label': '刷牙记录', 'color': const Color(0xFF26A69A), 'page': const BrushHistoryPage()},

用Map数组定义分类配置。

每个分类包含图标、文字标签、主题颜色和目标页面四个属性。

      {'icon': Icons.water_drop, 'label': '漱口记录', 'color': const Color(0xFF42A5F5), 'page': const MouthwashPage()},
      {'icon': Icons.linear_scale, 'label': '牙线记录', 'color': const Color(0xFFAB47BC), 'page': const FlossPage()},

漱口记录用蓝色,牙线记录用紫色。

不同分类使用不同颜色,视觉上更容易区分。

      {'icon': Icons.restaurant, 'label': '饮食记录', 'color': const Color(0xFFFF7043), 'page': const DietRecordPage()},
      {'icon': Icons.medical_services, 'label': '检查记录', 'color': const Color(0xFF5C6BC0), 'page': const OralCheckPage()},
      {'icon': Icons.warning_amber, 'label': '口腔问题', 'color': const Color(0xFFEF5350), 'page': const OralIssuePage()},
    ];

饮食记录用橙色,检查记录用靛蓝色,口腔问题用红色表示警示。

颜色的选择既要美观,也要符合功能的语义含义。

    return GridView.builder(
      shrinkWrap: true,
      physics: const NeverScrollableScrollPhysics(),

GridView.builder创建网格布局。

shrinkWrap让高度自适应内容,NeverScrollableScrollPhysics禁用自身滚动避免冲突。

      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 3,
        childAspectRatio: 1,
        crossAxisSpacing: 12,
        mainAxisSpacing: 12,
      ),

3列布局,childAspectRatio为1表示正方形格子。

横向和纵向间距都是12像素,格子之间保持适当距离。

      itemCount: categories.length,
      itemBuilder: (context, index) {
        final category = categories[index];
        return GestureDetector(
          onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => category['page'] as Widget)),

itemBuilder根据索引构建每个格子。

点击时使用Navigator.push跳转到对应的详情页面。

          child: Container(
            decoration: BoxDecoration(
              color: Colors.white,
              borderRadius: BorderRadius.circular(12),
              boxShadow: [BoxShadow(color: Colors.grey.shade200, blurRadius: 5)],
            ),

白色背景卡片,12像素圆角,浅色阴影。

这种设计和日历卡片风格统一,整体视觉协调。

            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Container(
                  padding: const EdgeInsets.all(10),
                  decoration: BoxDecoration(
                    color: (category['color'] as Color).withOpacity(0.1),
                    shape: BoxShape.circle,
                  ),
                  child: Icon(category['icon'] as IconData, color: category['color'] as Color, size: 28),
                ),

图标用浅色圆形背景包裹,颜色是分类主色的10%透明度。

这种设计让图标更加突出,同时保持整体的轻盈感。

                const SizedBox(height: 8),
                Text(category['label'] as String, style: const TextStyle(fontSize: 13)),
              ],
            ),
          ),
        );
      },
    );
  }

图标下方显示分类名称,13像素字体大小适中。

Column居中对齐让内容在格子中央显示。

今日记录列表实现

展示当天的所有护理记录,让用户快速回顾今天的护理情况。

  Widget _buildTodayRecords(AppProvider provider) {
    final today = DateTime.now();

获取当前日期时间,用于筛选今天的记录。

    final todayBrush = provider.brushRecords.where((r) =>
      r.dateTime.year == today.year &&
      r.dateTime.month == today.month &&
      r.dateTime.day == today.day
    ).toList();

筛选今天的刷牙记录。

where方法配合年月日三个条件比较,精确筛选出当天的数据。

    final todayMouthwash = provider.mouthwashRecords.where((r) =>
      r.dateTime.year == today.year &&
      r.dateTime.month == today.month &&
      r.dateTime.day == today.day
    ).toList();

同样的方式筛选今天的漱口记录。

toList()把筛选结果转成List,方便后续遍历。

    final todayFloss = provider.flossRecords.where((r) =>
      r.dateTime.year == today.year &&
      r.dateTime.month == today.month &&
      r.dateTime.day == today.day
    ).toList();

筛选今天的牙线使用记录。

三种记录分别筛选,然后合并显示。

    if (todayBrush.isEmpty && todayMouthwash.isEmpty && todayFloss.isEmpty) {
      return Container(
        padding: const EdgeInsets.all(20),
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.circular(12),
        ),
        child: const Center(
          child: Text('今日暂无记录,快去护理吧!', style: TextStyle(color: Colors.grey)),
        ),
      );
    }

如果今天没有任何记录,显示友好的空状态提示。

鼓励用户去进行口腔护理,而不是冷冰冰的"暂无数据"。

    return Column(
      children: [
        ...todayBrush.map((record) => _buildRecordItem(
          icon: Icons.brush,
          color: const Color(0xFF26A69A),
          title: '刷牙',
          subtitle: '${record.durationSeconds ~/ 60}${record.durationSeconds % 60}秒 · 评分${record.score}',
          time: DateFormat('HH:mm').format(record.dateTime),
        )),

展开运算符…把map结果展开成多个Widget。

刷牙记录显示时长和评分,时间用DateFormat格式化成"HH:mm"格式。

        ...todayMouthwash.map((record) => _buildRecordItem(
          icon: Icons.water_drop,
          color: const Color(0xFF42A5F5),
          title: '漱口',
          subtitle: '${record.productName} · ${record.durationSeconds}秒',
          time: DateFormat('HH:mm').format(record.dateTime),
        )),

漱口记录显示使用的产品名称和漱口时长。

蓝色图标和刷牙的绿色区分开来。

        ...todayFloss.map((record) => _buildRecordItem(
          icon: Icons.linear_scale,
          color: const Color(0xFFAB47BC),
          title: '牙线',
          subtitle: record.type,
          time: DateFormat('HH:mm').format(record.dateTime),
        )),
      ],
    );
  }

牙线记录显示使用的牙线类型。

紫色图标让三种记录在视觉上容易区分。

通用记录项组件

抽取通用的记录项组件,避免重复代码,提高可维护性。

  Widget _buildRecordItem({
    required IconData icon,
    required Color color,
    required String title,
    required String subtitle,
    required String time,
  }) {

使用命名参数,required关键字确保调用时必须传入这些参数。

这种写法让代码更清晰,调用时也更容易理解每个参数的含义。

    return Container(
      margin: const EdgeInsets.only(bottom: 8),
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(12),
      ),

每条记录是一个白色圆角卡片。

底部有8像素间距,让多条记录之间有适当的分隔。

      child: Row(
        children: [
          Container(
            padding: const EdgeInsets.all(10),
            decoration: BoxDecoration(
              color: color.withOpacity(0.1),
              shape: BoxShape.circle,
            ),
            child: Icon(icon, color: color, size: 24),
          ),

左侧是带背景的图标,颜色根据记录类型变化。

圆形浅色背景让图标更加醒目。

          const SizedBox(width: 12),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
                Text(subtitle, style: TextStyle(color: Colors.grey.shade600, fontSize: 13)),
              ],
            ),
          ),

中间是记录的标题和详情。

Expanded让文字区域占据剩余空间,标题加粗,详情用灰色小字。

          Text(time, style: TextStyle(color: Colors.grey.shade500, fontSize: 13)),
        ],
      ),
    );
  }
}

右侧显示记录的时间,灰色小字不抢眼但信息完整。

整个布局使用Row横向排列,结构清晰。

小结

记录页面通过日历和列表的组合,为用户提供了完整的护理记录查看体验。日历组件支持周视图和月视图切换,有记录的日期会显示小绿点标记,让用户一眼看出哪些天坚持了护理。分类网格让用户快速进入各类记录的详情页面,今日记录列表则展示当天的护理情况。整个页面的设计注重信息的层次和可读性,配色方案与首页保持一致,给用户统一的视觉体验。


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

Logo

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

更多推荐