在这里插入图片描述

前言

定期的口腔检查是预防和早期发现口腔疾病的重要手段。在口腔护理应用中,记录每次检查的详细信息,包括检查类型、医院、医生、检查结果和建议等,可以帮助用户追踪自己的口腔健康状况,也便于在复诊时提供参考。

本文将介绍如何在 Flutter 中实现一个信息丰富的检查记录功能。

功能规划

检查记录页面需要实现以下功能:

  • 记录列表:展示所有检查记录
  • 详细信息:包括检查类型、日期、医院、医生、结果、问题、建议等
  • 卡片布局:使用卡片形式展示每条记录的完整信息
  • 添加记录:支持添加新的检查记录

页面基础结构

检查记录页面使用 StatelessWidget 实现:

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('检查记录')),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _showAddDialog(context),
        backgroundColor: const Color(0xFF26A69A),
        child: const Icon(Icons.add),
      ),

页面结构与其他记录页面保持一致,包含标题栏和浮动操作按钮。

记录列表构建

使用 Consumer 监听数据变化:

      body: Consumer<AppProvider>(
        builder: (context, provider, _) {
          if (provider.checkRecords.isEmpty) {
            return const Center(child: Text('暂无检查记录'));
          }

          return ListView.builder(
            padding: const EdgeInsets.all(16),
            itemCount: provider.checkRecords.length,
            itemBuilder: (context, index) {
              final record = provider.checkRecords[index];

标准的空状态检查和列表构建模式。

检查记录卡片

检查记录包含较多信息,使用卡片形式展示:

              return Container(
                margin: const EdgeInsets.only(bottom: 16),
                padding: const EdgeInsets.all(16),
                decoration: BoxDecoration(
                  color: Colors.white,
                  borderRadius: BorderRadius.circular(12),
                ),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Row(
                      children: [
                        Container(
                          padding: const EdgeInsets.all(10),
                          decoration: BoxDecoration(
                            color: const Color(0xFF5C6BC0).withOpacity(0.1),
                            shape: BoxShape.circle,
                          ),
                          child: const Icon(Icons.medical_services, 
                              color: Color(0xFF5C6BC0)),
                        ),

检查记录使用靛蓝色作为主色调,医疗服务图标直观地表示检查这一行为。

卡片头部展示检查类型和日期:

                        const SizedBox(width: 12),
                        Expanded(
                          child: Column(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                              Text(record.checkType, 
                                   style: const TextStyle(fontWeight: FontWeight.bold, 
                                       fontSize: 16)),
                              Text(DateFormat('yyyy-MM-dd').format(record.date), 
                                   style: TextStyle(color: Colors.grey.shade600)),
                            ],
                          ),
                        ),
                      ],
                    ),

检查类型作为主标题,日期作为副标题,形成清晰的信息层次。

分割线和详细信息区域:

                    const SizedBox(height: 12),
                    const Divider(height: 1),
                    const SizedBox(height: 12),
                    _buildInfoRow('医院', record.hospital ?? '未填写'),
                    _buildInfoRow('医生', record.doctorName ?? '未填写'),
                    _buildInfoRow('结果', record.result),
                    if (record.issues.isNotEmpty)
                      _buildInfoRow('问题', record.issues.join('、')),
                    if (record.advice != null)
                      _buildInfoRow('建议', record.advice!),
                  ],
                ),
              );
            },
          );
        },
      ),
    );
  }

使用分割线将头部和详细信息分开。条件渲染确保只有存在数据时才显示对应字段。

信息行组件

封装信息行组件用于展示键值对:

Widget _buildInfoRow(String label, String value) {
  return Padding(
    padding: const EdgeInsets.only(bottom: 8),
    child: Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        SizedBox(
          width: 50,
          child: Text('$label:', style: TextStyle(color: Colors.grey.shade600)),
        ),
        Expanded(child: Text(value)),
      ],
    ),
  );
}

标签使用固定宽度,值使用 Expanded 占据剩余空间。crossAxisAlignment.start 确保多行文本时对齐顶部。

添加记录对话框

添加检查记录的对话框(简化版):

void _showAddDialog(BuildContext context) {
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(content: Text('添加检查记录功能开发中')),
  );
}

实际项目中需要实现完整的表单对话框。

数据模型定义

检查记录的数据模型:

class CheckRecord {
  final String id;
  final DateTime date;
  final String checkType;
  final String? hospital;
  final String? doctorName;
  final String result;
  final List<String> issues;
  final String? advice;

  CheckRecord({
    String? id,
    required this.date,
    required this.checkType,
    this.hospital,
    this.doctorName,
    required this.result,
    this.issues = const [],
    this.advice,
  }) : id = id ?? DateTime.now().millisecondsSinceEpoch.toString();
}

模型包含日期、检查类型、医院、医生、结果、问题列表和建议等字段。问题使用列表存储,支持记录多个问题。

Provider 数据管理

AppProvider 中管理检查记录:

List<CheckRecord> _checkRecords = [];
List<CheckRecord> get checkRecords => _checkRecords;

void addCheckRecord(CheckRecord record) {
  _checkRecords.insert(0, record);
  notifyListeners();
}

与其他记录类型保持一致的数据管理模式。

测试数据生成

生成测试数据:

void initTestData() {
  _checkRecords = [
    CheckRecord(
      date: DateTime.now().subtract(const Duration(days: 30)),
      checkType: '常规检查',
      hospital: '市口腔医院',
      doctorName: '张医生',
      result: '整体健康',
      issues: ['轻微牙结石'],
      advice: '建议半年后洗牙',
    ),
    CheckRecord(
      date: DateTime.now().subtract(const Duration(days: 180)),
      checkType: '洗牙',
      hospital: '市口腔医院',
      doctorName: '李医生',
      result: '清洁完成',
      advice: '注意日常刷牙方式',
    ),
  ];
}

测试数据包含不同类型的检查记录,便于验证显示效果。

完整添加对话框实现

实现完整的添加检查记录对话框:

void _showAddDialog(BuildContext context) {
  final typeController = TextEditingController();
  final hospitalController = TextEditingController();
  final doctorController = TextEditingController();
  final resultController = TextEditingController();
  final adviceController = TextEditingController();
  DateTime selectedDate = DateTime.now();

  showDialog(
    context: context,
    builder: (ctx) => StatefulBuilder(
      builder: (context, setState) => AlertDialog(
        title: const Text('添加检查记录'),
        content: SingleChildScrollView(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              DropdownButtonFormField<String>(
                decoration: const InputDecoration(labelText: '检查类型'),
                items: const [
                  DropdownMenuItem(value: '常规检查', child: Text('常规检查')),
                  DropdownMenuItem(value: '洗牙', child: Text('洗牙')),
                  DropdownMenuItem(value: '补牙', child: Text('补牙')),
                  DropdownMenuItem(value: '拔牙', child: Text('拔牙')),
                  DropdownMenuItem(value: '其他', child: Text('其他')),
                ],
                onChanged: (v) => typeController.text = v ?? '',
              ),
              const SizedBox(height: 12),
              TextField(
                controller: hospitalController,
                decoration: const InputDecoration(labelText: '医院'),
              ),
              const SizedBox(height: 12),
              TextField(
                controller: doctorController,
                decoration: const InputDecoration(labelText: '医生'),
              ),
              const SizedBox(height: 12),
              TextField(
                controller: resultController,
                decoration: const InputDecoration(labelText: '检查结果'),
                maxLines: 2,
              ),
              const SizedBox(height: 12),
              TextField(
                controller: adviceController,
                decoration: const InputDecoration(labelText: '医生建议'),
                maxLines: 2,
              ),
            ],
          ),
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(ctx),
            child: const Text('取消'),
          ),
          ElevatedButton(
            onPressed: () {
              if (typeController.text.isEmpty || resultController.text.isEmpty) {
                return;
              }
              final record = CheckRecord(
                date: selectedDate,
                checkType: typeController.text,
                hospital: hospitalController.text.isEmpty 
                    ? null : hospitalController.text,
                doctorName: doctorController.text.isEmpty 
                    ? null : doctorController.text,
                result: resultController.text,
                advice: adviceController.text.isEmpty 
                    ? null : adviceController.text,
              );
              context.read<AppProvider>().addCheckRecord(record);
              Navigator.pop(ctx);
            },
            child: const Text('保存'),
          ),
        ],
      ),
    ),
  );
}

表单包含检查类型下拉选择、医院、医生、结果和建议等输入字段。

检查类型图标映射

可以为不同检查类型使用不同图标:

IconData _getCheckTypeIcon(String type) {
  switch (type) {
    case '常规检查':
      return Icons.search;
    case '洗牙':
      return Icons.cleaning_services;
    case '补牙':
      return Icons.build;
    case '拔牙':
      return Icons.remove_circle;
    default:
      return Icons.medical_services;
  }
}

不同图标帮助用户快速识别检查类型。

检查提醒功能

可以根据上次检查时间提醒用户:

String getCheckReminder() {
  if (_checkRecords.isEmpty) {
    return '您还没有检查记录,建议每半年进行一次口腔检查';
  }
  
  final lastCheck = _checkRecords.first.date;
  final daysSince = DateTime.now().difference(lastCheck).inDays;
  
  if (daysSince > 180) {
    return '距离上次检查已超过6个月,建议尽快预约检查';
  } else if (daysSince > 150) {
    return '距离上次检查已${daysSince}天,建议近期安排检查';
  } else {
    return '上次检查在${daysSince}天前,继续保持定期检查的好习惯';
  }
}

根据检查间隔给出个性化提醒。

检查统计信息

统计检查相关数据:

Map<String, int> getCheckStats() {
  final stats = <String, int>{};
  for (var record in _checkRecords) {
    stats[record.checkType] = (stats[record.checkType] ?? 0) + 1;
  }
  return stats;
}

int get totalCheckCount => _checkRecords.length;

int get thisYearCheckCount {
  final thisYear = DateTime.now().year;
  return _checkRecords.where((r) => r.date.year == thisYear).length;
}

统计各类型检查次数和年度检查次数。

页面顶部提醒卡片

在列表上方添加检查提醒:

Container(
  padding: const EdgeInsets.all(16),
  margin: const EdgeInsets.only(bottom: 16),
  decoration: BoxDecoration(
    color: const Color(0xFF5C6BC0).withOpacity(0.1),
    borderRadius: BorderRadius.circular(12),
  ),
  child: Row(
    children: [
      const Icon(Icons.info_outline, color: Color(0xFF5C6BC0)),
      const SizedBox(width: 12),
      Expanded(
        child: Text(
          getCheckReminder(),
          style: const TextStyle(color: Color(0xFF5C6BC0)),
        ),
      ),
    ],
  ),
)

提醒卡片使用主题色背景,突出显示检查建议。

问题标签展示

将问题列表以标签形式展示:

if (record.issues.isNotEmpty)
  Wrap(
    spacing: 8,
    runSpacing: 4,
    children: record.issues.map((issue) => Container(
      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
      decoration: BoxDecoration(
        color: Colors.orange.withOpacity(0.1),
        borderRadius: BorderRadius.circular(4),
      ),
      child: Text(issue, 
          style: const TextStyle(color: Colors.orange, fontSize: 12)),
    )).toList(),
  ),

使用 Wrap 组件让标签自动换行,橙色标签突出显示问题。

检查记录详情页

可以添加详情页展示更多信息:

void _showDetailPage(BuildContext context, CheckRecord record) {
  Navigator.push(
    context,
    MaterialPageRoute(
      builder: (context) => CheckDetailPage(record: record),
    ),
  );
}

详情页可以展示完整的检查报告和历史对比。

数据导出功能

支持导出检查记录:

String exportCheckRecords() {
  final buffer = StringBuffer();
  buffer.writeln('口腔检查记录');
  buffer.writeln('导出时间:${DateFormat('yyyy-MM-dd HH:mm').format(DateTime.now())}');
  buffer.writeln('');
  
  for (var record in _checkRecords) {
    buffer.writeln('检查类型:${record.checkType}');
    buffer.writeln('检查日期:${DateFormat('yyyy-MM-dd').format(record.date)}');
    buffer.writeln('医院:${record.hospital ?? '未填写'}');
    buffer.writeln('医生:${record.doctorName ?? '未填写'}');
    buffer.writeln('结果:${record.result}');
    if (record.issues.isNotEmpty) {
      buffer.writeln('问题:${record.issues.join('')}');
    }
    if (record.advice != null) {
      buffer.writeln('建议:${record.advice}');
    }
    buffer.writeln('---');
  }
  
  return buffer.toString();
}

导出功能便于用户保存或分享检查记录。

空状态优化

为空状态添加引导:

if (provider.checkRecords.isEmpty) {
  return Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Icon(Icons.medical_services, size: 64, color: Colors.grey.shade300),
        const SizedBox(height: 16),
        Text('暂无检查记录', style: TextStyle(color: Colors.grey.shade500)),
        const SizedBox(height: 8),
        Text('建议每半年进行一次口腔检查',
             style: TextStyle(color: Colors.grey.shade400, fontSize: 12)),
        const SizedBox(height: 16),
        ElevatedButton.icon(
          onPressed: () => _showAddDialog(context),
          icon: const Icon(Icons.add),
          label: const Text('添加检查记录'),
          style: ElevatedButton.styleFrom(
            backgroundColor: const Color(0xFF5C6BC0),
          ),
        ),
      ],
    ),
  );
}

空状态页面添加健康提示和添加按钮。

总结

本文详细介绍了口腔护理 App 中检查记录功能的实现。通过卡片式布局和丰富的信息展示,我们构建了一个实用的检查记录管理页面。核心技术点包括:

  • 使用卡片布局展示多字段信息
  • 封装信息行组件提高代码复用
  • 条件渲染处理可选字段
  • 使用列表存储多个问题

检查记录功能帮助用户追踪口腔健康状况,是口腔护理应用的重要组成部分。

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

Logo

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

更多推荐