
引言
定期的口腔检查和治疗是维护口腔健康的重要环节。一个好的口腔护理应用不仅要帮助用户记录日常护理情况,还应该提供便捷的预约管理功能,让用户能够轻松安排和追踪自己的就诊计划。
本文将详细讲解如何在 Flutter 中实现一个完整的预约管理页面,包括预约列表展示、状态分类、新建预约对话框等功能。
功能概述
预约管理页面需要实现以下核心功能:
- 分类展示:将预约按"待就诊"和"已完成"两种状态分类显示
- 预约卡片:展示预约的详细信息,包括就诊目的、时间、医院、医生等
- 新建预约:通过浮动按钮触发新建预约对话框
- 日期时间选择:支持选择预约的具体日期和时间
页面基础结构
预约管理页面使用 StatelessWidget 实现,因为页面本身不需要维护内部状态,所有数据都来自 AppProvider:
dart
class AppointmentPage extends StatelessWidget {
const AppointmentPage({super.key});
@override
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),
),
页面使用 Scaffold 作为基础框架,配置了标题栏和浮动操作按钮。浮动按钮使用应用的主题色,点击时触发新建预约对话框。
数据获取与分类
使用 Consumer 监听 AppProvider 的数据变化,并将预约按状态分类:
dart
body: Consumer<AppProvider>(
builder: (context, provider, _) {
final pending = provider.appointments
.where((a) => a.status == 'pending').toList();
final completed = provider.appointments
.where((a) => a.status == 'completed').toList();
通过 where 方法过滤出待就诊和已完成的预约列表。这种函数式的写法简洁明了,易于理解和维护。
页面主体布局采用 SingleChildScrollView 包裹 Column,确保内容可以滚动:
dart
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('待就诊',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
if (pending.isEmpty)
_buildEmptyCard('暂无待就诊预约')
else
...pending.map((a) => _buildAppointmentCard(context, a, true)),
const SizedBox(height: 24),
const Text('已完成',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
if (completed.isEmpty)
_buildEmptyCard('暂无已完成预约')
else
...completed.map((a) => _buildAppointmentCard(context, a, false)),
],
),
);
},
),
);
}
使用展开运算符 ... 将预约列表映射为卡片组件列表,这是 Dart 中处理列表嵌入的优雅方式。
空状态卡片
当某个分类下没有预约时,显示一个友好的空状态提示:
dart
Widget _buildEmptyCard(String text) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Text(text, style: const TextStyle(color: Colors.grey)),
),
);
}
空状态卡片使用灰色文字,视觉上比较柔和,不会给用户造成压迫感。圆角白色背景与其他卡片保持一致的风格。
预约卡片组件
预约卡片是页面的核心组件,需要展示丰富的信息:
dart
Widget _buildAppointmentCard(BuildContext context,
Appointment appointment, bool isPending) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: isPending ? Border.all(color: const Color(0xFF26A69A)) : null,
),
待就诊的预约卡片添加主题色边框,与已完成的预约形成视觉区分。这种设计让用户一眼就能识别出需要关注的预约。
卡片头部展示预约的主要信息:
dart
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: isPending
? const Color(0xFF26A69A).withOpacity(0.1)
: Colors.grey.shade100,
shape: BoxShape.circle,
),
child: Icon(
Icons.calendar_month,
color: isPending ? const Color(0xFF26A69A) : Colors.grey,
),
),
const SizedBox(width: 12),
图标容器根据预约状态使用不同的背景色和图标色,待就诊使用主题色,已完成使用灰色。
预约标题和时间的展示:
dart
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(appointment.purpose,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
Text(
DateFormat('yyyy-MM-dd HH:mm').format(appointment.dateTime),
style: TextStyle(color: Colors.grey.shade600),
),
],
),
),
使用 intl 包的 DateFormat 来格式化日期时间,这是 Flutter 中处理日期格式化的标准方式。
待就诊状态标签:
dart
if (isPending)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.orange.shade100,
borderRadius: BorderRadius.circular(4),
),
child: Text('待就诊',
style: TextStyle(color: Colors.orange.shade700, fontSize: 12)),
),
],
),
状态标签使用橙色系配色,与主题色形成对比,突出提醒用户这是待处理的预约。
卡片底部展示医院和医生信息:
dart
const SizedBox(height: 12),
const Divider(height: 1),
const SizedBox(height: 12),
Row(
children: [
const Icon(Icons.local_hospital, size: 16, color: Colors.grey),
const SizedBox(width: 4),
Text(appointment.hospital,
style: TextStyle(color: Colors.grey.shade600)),
const SizedBox(width: 16),
const Icon(Icons.person, size: 16, color: Colors.grey),
const SizedBox(width: 4),
Text(appointment.doctorName,
style: TextStyle(color: Colors.grey.shade600)),
],
),
],
),
);
}
使用分割线将主要信息和次要信息分开,图标配合文字让信息更加直观易读。
新建预约对话框
点击浮动按钮时弹出新建预约对话框,这是一个相对复杂的交互组件:
dart
void _showAddDialog(BuildContext context) {
final purposeController = TextEditingController();
final doctorController = TextEditingController();
final hospitalController = TextEditingController(text: '市口腔医院');
DateTime selectedDate = DateTime.now().add(const Duration(days: 1));
初始化文本控制器和默认日期。医院字段预填了默认值,日期默认为明天,这些都是为了减少用户的输入负担。
对话框使用 StatefulBuilder 来管理内部状态:
dart
showDialog(
context: context,
builder: (ctx) => StatefulBuilder(
builder: (context, setState) => AlertDialog(
title: const Text('新建预约'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: purposeController,
decoration: const InputDecoration(labelText: '就诊目的'),
),
const SizedBox(height: 12),
TextField(
controller: hospitalController,
decoration: const InputDecoration(labelText: '医院'),
),
const SizedBox(height: 12),
TextField(
controller: doctorController,
decoration: const InputDecoration(labelText: '医生'),
),
StatefulBuilder 允许我们在 StatelessWidget 中创建有状态的对话框,这是一个非常实用的技巧。
日期时间选择器的实现:
dart
const SizedBox(height: 12),
ListTile(
contentPadding: EdgeInsets.zero,
title: const Text('预约时间'),
subtitle: Text(DateFormat('yyyy-MM-dd HH:mm').format(selectedDate)),
trailing: const Icon(Icons.calendar_today),
onTap: () async {
final date = await showDatePicker(
context: context,
initialDate: selectedDate,
firstDate: DateTime.now(),
lastDate: DateTime.now().add(const Duration(days: 365)),
);
使用 ListTile 作为日期选择的触发器,点击后先弹出日期选择器。firstDate 设为当前日期,防止用户选择过去的日期。
时间选择的处理:
dart
if (date != null) {
final time = await showTimePicker(
context: context,
initialTime: TimeOfDay.fromDateTime(selectedDate),
);
if (time != null) {
setState(() {
selectedDate = DateTime(
date.year, date.month, date.day,
time.hour, time.minute
);
});
}
}
},
),
],
),
),
日期选择完成后继续弹出时间选择器,两者结合生成完整的预约时间。使用 setState 更新界面显示。
对话框的操作按钮:
dart
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('取消')
),
ElevatedButton(
onPressed: () {
if (purposeController.text.isEmpty ||
doctorController.text.isEmpty) {
return;
}
final appointment = Appointment(
dateTime: selectedDate,
doctorName: doctorController.text,
hospital: hospitalController.text,
purpose: purposeController.text,
);
context.read<AppProvider>().addAppointment(appointment);
Navigator.pop(ctx);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('预约已创建'))
);
},
child: const Text('保存'),
),
],
),
),
);
}
保存前进行简单的表单验证,确保必填字段不为空。创建成功后关闭对话框并显示提示消息。
数据模型定义
预约功能依赖的数据模型在 oral_models.dart 中定义:
dart
class Appointment {
final String id;
final DateTime dateTime;
final String doctorName;
final String hospital;
final String purpose;
final String status;
Appointment({
String? id,
required this.dateTime,
required this.doctorName,
required this.hospital,
required this.purpose,
this.status = 'pending',
}) : id = id ?? DateTime.now().millisecondsSinceEpoch.toString();
}
模型使用工厂构造函数自动生成唯一 ID,状态默认为待就诊。这种设计简化了创建预约时的代码。
Provider 中的数据操作
在 AppProvider 中实现预约的增删改查:
dart
List<Appointment> _appointments = [];
List<Appointment> get appointments => _appointments;
void addAppointment(Appointment appointment) {
_appointments.add(appointment);
notifyListeners();
}
void updateAppointmentStatus(String id, String status) {
final index = _appointments.indexWhere((a) => a.id == id);
if (index != -1) {
final old = _appointments[index];
_appointments[index] = Appointment(
id: old.id,
dateTime: old.dateTime,
doctorName: old.doctorName,
hospital: old.hospital,
purpose: old.purpose,
status: status,
);
notifyListeners();
}
}
每次数据变化后调用 notifyListeners() 通知界面更新。由于 Dart 中的类是不可变的,更新状态时需要创建新的实例。
日期格式化处理
项目中使用 intl 包进行日期格式化,需要在 pubspec.yaml 中添加依赖:
yaml
dependencies:
intl: ^0.18.0
然后在代码中导入使用:
dart
import 'package:intl/intl.dart';
// 格式化日期时间
DateFormat('yyyy-MM-dd HH:mm').format(dateTime)
// 只格式化日期
DateFormat('yyyy-MM-dd').format(dateTime)
// 中文格式
DateFormat('MM月dd日 HH:mm').format(dateTime)
DateFormat 支持多种格式化模式,可以根据需求灵活选择。
用户体验优化
为了提升用户体验,我们在设计中考虑了以下几点:
视觉层次:通过颜色、边框、阴影等元素区分不同状态的预约,让用户快速识别重要信息。
默认值设置:医院字段预填默认值,日期默认为明天,减少用户输入。
即时反馈:创建预约后显示 SnackBar 提示,让用户知道操作已成功。
表单验证:必填字段为空时不允许提交,避免创建无效数据。
扩展功能建议
基于当前实现,可以考虑添加以下功能:
- 预约提醒:在预约时间前发送通知提醒用户
- 预约修改:支持修改已创建的预约信息
- 预约取消:支持取消待就诊的预约
- 历史记录:查看更早之前的就诊记录
- 医生评价:就诊完成后对医生进行评价
状态管理思考
在这个页面中,我们使用了 Provider 进行状态管理。对于预约这种需要持久化的数据,实际项目中还需要考虑:
dart
// 数据持久化示例
void saveAppointments() async {
final prefs = await SharedPreferences.getInstance();
final jsonList = _appointments.map((a) => a.toJson()).toList();
await prefs.setString('appointments', jsonEncode(jsonList));
}
void loadAppointments() async {
final prefs = await SharedPreferences.getInstance();
final jsonStr = prefs.getString('appointments');
if (jsonStr != null) {
final jsonList = jsonDecode(jsonStr) as List;
_appointments = jsonList.map((j) => Appointment.fromJson(j)).toList();
notifyListeners();
}
}
使用 SharedPreferences 或数据库来持久化预约数据,确保应用重启后数据不丢失。
总结
本文详细介绍了口腔护理 App 中预约管理功能的实现。通过合理的 UI 设计和状态管理,我们实现了一个功能完善、体验友好的预约管理页面。核心技术点包括:
- 使用
Consumer监听数据变化 - 通过
StatefulBuilder在对话框中管理状态 - 结合日期和时间选择器实现完整的时间选择
- 使用
intl包格式化日期显示
这些技术和设计思路可以应用到其他类似的列表管理功能中。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net