flutter_for_openharmony城市井盖地图app实战+日程排班实现
本文介绍了一个基于Flutter的井盖巡检APP日程排班功能实现。该功能采用"日历+列表"组合设计,通过TableCalendar和ListView.separated组件实现日期选择和工单展示。核心特性包括:日期可视化选择、工单精准过滤、时间轻量化显示和状态清晰标识。页面维护两个日期状态(_focusedDay和_selectedDay)来支撑交互逻辑,并通过条件过滤确保列表

1. 这个功能解决什么问题
日程排班是井盖巡检类APP的核心功能之一,专门为巡检人员的日常工作场景设计,核心价值体现在以下维度:
- 日期可视化:通过日历组件让巡检人员直观选择想要查看的日期,替代传统的文本日期选择框,降低操作成本
- 工单精准展示:选中日期后自动过滤并展示当天的所有工单,避免跨日期信息干扰
- 信息轻量化:对工单时间仅展示时分维度,贴合巡检人员“按时间段执行任务”的核心诉求
- 状态清晰化:在工单列表中直接展示状态文字,巡检人员可快速判断工单执行阶段
这个页面是典型的“日历+列表”组合,适合做 TableCalendar + ListView.separated 的最佳实践示例。
2. 相关文件一览
功能落地涉及两个核心文件,职责划分清晰:
lib/feature_pages.dart:核心页面文件,包含CalendarSchedulePage类,负责日程排班页面的UI渲染和交互逻辑lib/mock_data.dart:模拟数据文件,通过buildMockWorkOrders方法生成测试用的工单数据,便于功能调试
3. 状态定义
在 CalendarSchedulePage 页面中,需要维护两个核心日期状态,支撑日历的交互逻辑:
class _CalendarSchedulePageState extends State<CalendarSchedulePage> {
// 日历当前聚焦的月份/日期
DateTime _focusedDay = DateTime.now();
// 用户手动选中的具体日期(可为空)
DateTime? _selectedDay;
}
状态设计的核心考量点:
_focusedDay:用于标识日历组件当前展示的月份,即使未选中任何日期,也能基于该值展示对应月份的工单_selectedDay:存储用户主动选择的日期,初始值为null,体现“可选可不选”的灵活交互设计- 初始值均设为当前时间,符合“打开页面默认查看今日工单”的用户习惯
4. 事件过滤逻辑
页面构建时,需根据选中/聚焦日期过滤工单,保证列表展示的精准性:
// 兜底逻辑:未选中日期时使用聚焦日期
final selected = _selectedDay ?? _focusedDay;
// 过滤当月当天的工单数据
final events = buildMockWorkOrders()
.where((e) => e.dueAt.day == selected.day
&& e.dueAt.month == selected.month)
.take(6) // 限制最多6条,避免列表过长
.toList(growable: false);
过滤逻辑的设计细节:
- 空值兜底:使用
??运算符确保selected始终有值,避免空指针异常 - 过滤维度:仅按“日”和“月”过滤,暂不考虑年份(适配巡检工单按自然月管理的业务场景)
- 数量限制:通过
take(6)限制列表长度,防止大量数据导致页面滚动卡顿 - 不可变列表:
growable: false创建固定长度列表,提升性能
5. 日历组件
使用 TableCalendar 实现日历展示和日期选择功能,核心配置如下:
TableCalendar(
// 日历时间范围:2020-2030年
firstDay: DateTime.utc(2020, 1, 1),
lastDay: DateTime.utc(2030, 12, 31),
focusedDay: _focusedDay,
// 判断日期是否被选中的条件
selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
// 日期选中回调
onDaySelected: (selectedDay, focusedDay) {
setState(() {
_selectedDay = selectedDay;
_focusedDay = focusedDay;
});
},
)
日历组件的关键配置说明:
- 时间范围:
firstDay和lastDay限定日历可操作的时间区间,避免用户选择无效日期 - 选中判断:
selectedDayPredicate通过isSameDay方法精准判断日期是否选中(包含年月日对比) - 状态更新:
onDaySelected回调中调用setState,触发页面重建,实现工单列表的实时刷新 - 聚焦同步:选中日期时同步更新
_focusedDay,保证日历视图与选中日期联动
6. 事件列表
使用 ListView.separated 展示过滤后的工单列表,提升列表渲染效率:
ListView.separated(
itemCount: events.length,
// 分隔线:高度1的灰色分割线
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, i) {
final e = events[i];
// 仅格式化时分,简化时间展示
final due = DateFormat('HH:mm').format(e.dueAt);
return ListTile(
leading: const Icon(Icons.event_note_rounded),
title: Text(e.title),
subtitle: Text('${e.district} · $due · ${e.status}'),
);
},
)
列表展示的体验优化点:
- 分隔线设计:
Divider(height: 1)实现细分割线,视觉上更清爽,区分不同工单 - 时间格式化:
DateFormat('HH:mm')只保留时分,符合巡检人员“按时间段执行任务”的查看习惯 - 信息聚合:
subtitle组合展示片区、时间、状态三大核心信息,单行呈现关键内容 - 视觉标识:
leading图标增强工单条目辨识度,符合移动端列表设计规范
7. 完整页面代码(核心片段拆解)
7.1 页面结构基础
首先定义页面的基础结构,包含状态管理和页面脚手架:
class CalendarSchedulePage extends StatefulWidget {
const CalendarSchedulePage({super.key});
State<CalendarSchedulePage> createState() => _CalendarSchedulePageState();
}
class _CalendarSchedulePageState extends State<CalendarSchedulePage> {
DateTime _focusedDay = DateTime.now();
DateTime? _selectedDay;
Widget build(BuildContext context) {
// 日期兜底逻辑
final selected = _selectedDay ?? _focusedDay;
页面结构设计说明:
- 采用
StatefulWidget:因包含日期选择、列表刷新等交互,需维护状态 - 状态变量私有化:
_focusedDay和_selectedDay作为私有变量,仅在当前状态类中修改 - 构建方法入口:
build方法中先处理日期兜底,再进行UI渲染
7.2 数据过滤与页面布局
接着实现数据过滤和页面的核心布局:
// 过滤选中日期的工单
final events = buildMockWorkOrders()
.where((e) => e.dueAt.day == selected.day
&& e.dueAt.month == selected.month)
.take(6)
.toList(growable: false);
return Scaffold(
appBar: AppBar(title: const Text('日程排班')),
body: Column(
children: [
// 日历组件区域
TableCalendar(
firstDay: DateTime.utc(2020, 1, 1),
lastDay: DateTime.utc(2030, 12, 31),
focusedDay: _focusedDay,
布局设计的核心思路:
- 垂直布局:
Column分为日历区和列表区,符合“上日历下列表”的视觉习惯 - 导航栏:
AppBar明确页面名称,符合Flutter页面设计规范 - 数据过滤前置:在布局渲染前完成数据过滤,避免布局过程中执行耗时操作
7.3 日历交互与列表渲染
最后完成日历交互配置和列表区域的渲染:
selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
onDaySelected: (selectedDay, focusedDay) {
setState(() {
_selectedDay = selectedDay;
_focusedDay = focusedDay;
});
},
),
const Divider(height: 1),
// 列表区域(可滚动)
Expanded(
child: ListView.separated(
itemCount: events.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, i) {
final e = events[i];
final due = DateFormat('HH:mm').format(e.dueAt);
关键实现细节:
Expanded包裹列表:让列表占满剩余空间,避免高度溢出- 日历与列表分隔:
Divider分隔日历和列表,视觉分区更清晰 - 列表项构建:逐行解析工单数据,格式化后展示核心信息
7.4 列表项完整渲染
列表项的最终渲染逻辑,包含所有核心信息展示:
return ListTile(
leading: const Icon(Icons.event_note_rounded),
title: Text(e.title),
subtitle: Text('${e.district} · $due · ${e.status}'),
);
},
),
),
],
),
);
}
}
列表项设计的用户体验考量:
- 图标引导:
leading图标快速提示条目类型为“工单事件” - 标题突出:
title展示工单标题,作为核心信息优先呈现 - 副标题聚合:整合片区、时间、状态,满足巡检人员快速浏览需求
8. 日程排班数据模型
为规范日程排班数据的管理,设计分层的数据模型,覆盖从事件到排班的全维度:
8.1 基础事件模型
定义 ScheduleEvent 类,描述单个巡检事件的所有属性:
class ScheduleEvent {
final String id; // 事件唯一标识
final String title; // 事件标题
final String description; // 事件描述
final DateTime startTime; // 开始时间
final DateTime endTime; // 结束时间
final EventType type; // 事件类型
final EventStatus status; // 事件状态
final String assigneeId; // 执行人ID
数据模型设计要点:
- 唯一标识:
id确保每个事件可被精准定位和修改 - 时间维度:区分
startTime和endTime,适配有持续时长的巡检任务 - 关联信息:
assigneeId关联执行人,支持按人员筛选工单 - 类型枚举:使用
EventType限定事件类型,避免字符串随意定义
8.2 事件模型补充属性
补充 ScheduleEvent 的剩余属性,覆盖位置、附件等扩展信息:
final String assigneeName; // 执行人姓名
final String district; // 所属片区
final String location; // 具体位置
final List<String> attachments; // 附件列表
final Map<String, dynamic> metadata; // 扩展元数据
const ScheduleEvent({
required this.id,
required this.title,
required this.description,
required this.startTime,
扩展属性设计说明:
- 位置信息:
district+location分级描述位置,适配井盖“片区+具体点位”的管理模式 - 附件支持:
attachments存储图片/文件路径,满足巡检过程中上传凭证的需求 - 元数据扩展:
metadata预留灵活字段,适配后续业务扩展
8.3 排班整体模型
定义 WorkSchedule 类,关联用户与当日所有事件:
class WorkSchedule {
final String id; // 排班ID
final String userId; // 用户ID
final String userName; // 用户名
final DateTime date; // 排班日期
final List<ScheduleEvent> events; // 当日事件列表
final ScheduleStatus status; // 排班状态
final DateTime createdAt; // 创建时间
final DateTime? updatedAt; // 更新时间(可选)
排班模型核心价值:
- 用户关联:
userId+userName绑定具体巡检人员 - 日期聚合:
date关联排班日期,events聚合当日所有事件 - 状态管理:
ScheduleStatus控制排班的发布/归档状态 - 时间追踪:
createdAt和updatedAt记录排班的生命周期
8.4 枚举类型定义
通过枚举限定类型和状态,避免魔法值,提升代码可维护性:
enum EventType {
inspection, // 井盖巡检
maintenance, // 维护保养
emergency, // 应急处理
meeting, // 工作会议
training, // 培训学习
}
enum EventStatus {
scheduled, // 已排班
inProgress, // 进行中
completed, // 已完成
cancelled, // 已取消
postponed, // 已延期
}
枚举设计的合理性:
- 贴合业务:
EventType覆盖巡检人员的核心工作类型 - 状态闭环:
EventStatus覆盖工单从创建到完成的全生命周期 - 语义清晰:枚举值命名采用英文,便于国际化,同时注释说明中文含义
8.5 排班状态枚举
补充排班整体状态的枚举,管理排班的发布状态:
enum ScheduleStatus {
draft, // 草稿
published, // 已发布
archived, // 已归档
}
排班状态设计逻辑:
- 草稿状态:支持排班的临时编辑,未发布时不生效
- 发布状态:正式生效的排班,巡检人员可查看
- 归档状态:历史排班,便于后续追溯和统计
9. 日程排班状态管理
使用 Provider 实现跨组件的状态管理,统一管理日程数据和交互状态:
9.1 状态容器基础
定义 ScheduleProvider 类,继承 ChangeNotifier,存储核心状态:
class ScheduleProvider extends ChangeNotifier {
// 日期-事件映射表
Map<DateTime, List<ScheduleEvent>> _events = {};
// 排班列表
List<WorkSchedule> _schedules = [];
// 日历聚焦日期
DateTime _focusedDay = DateTime.now();
// 选中日期
DateTime? _selectedDay;
// 加载状态
bool _loading = false;
// 错误信息
String? _error;
状态管理设计思路:
- 数据映射:
_events以日期为键,快速查询指定日期的所有事件 - 状态追踪:
_loading和_error管理数据加载状态,便于展示加载/错误UI - 通知机制:继承
ChangeNotifier,状态变化时通知组件刷新
9.2 状态访问器
提供只读访问器,避免外部直接修改私有状态:
// 只读访问器,外部仅能读取不能修改
Map<DateTime, List<ScheduleEvent>> get events => _events;
List<WorkSchedule> get schedules => _schedules;
DateTime get focusedDay => _focusedDay;
DateTime? get selectedDay => _selectedDay;
bool get loading => _loading;
String? get error => _error;
访问器设计的意义:
- 封装性:私有状态仅能通过类内方法修改,保证状态变更的可控性
- 只读性:外部只能读取状态,避免非法修改导致的状态不一致
- 一致性:所有状态访问通过统一入口,便于后续扩展权限控制
9.3 数据加载方法
实现 loadSchedules 方法,模拟异步加载日程数据:
Future<void> loadSchedules() async {
// 开始加载:更新状态并通知
_loading = true;
_error = null;
notifyListeners();
try {
// 模拟网络请求延迟
await Future.delayed(const Duration(seconds: 1));
// 生成模拟数据
_schedules = _generateMockSchedules();
_events = _generateEventsMap();
数据加载流程:
- 初始化状态:加载开始时重置错误、标记加载中,通知UI展示加载动画
- 模拟延迟:模拟真实网络请求的耗时,贴合实际业务场景
- 生成数据:调用模拟方法生成排班和事件数据
- 状态更新:加载完成后更新状态,通知UI刷新
9.4 加载异常处理
补充 loadSchedules 的异常处理逻辑,保证鲁棒性:
// 加载完成:更新状态并通知
_loading = false;
notifyListeners();
} catch (e) {
// 加载失败:记录错误并通知
_error = e.toString();
_loading = false;
notifyListeners();
}
}
异常处理的必要性:
- 错误捕获:捕获异步操作中的异常,避免应用崩溃
- 错误反馈:
_error存储错误信息,便于UI展示错误提示 - 状态重置:无论成功/失败,都将
_loading置为false,避免加载状态卡死
9.5 事件查询方法
实现 getEventsForDay 方法,快速获取指定日期的事件:
// 根据日期查询事件列表
List<ScheduleEvent> getEventsForDay(DateTime day) {
// 无事件时返回空列表,避免空指针
return _events[day] ?? [];
}
// 日期选中回调
void onDaySelected(DateTime selectedDay, DateTime focusedDay) {
_selectedDay = selectedDay;
_focusedDay = focusedDay;
notifyListeners();
}
事件查询优化点:
- 空值兜底:
?? []确保返回值始终为列表,外部无需判空 - 状态同步:
onDaySelected同步更新选中日期和聚焦日期,保证日历与列表联动 - 即时通知:状态修改后调用
notifyListeners,触发UI实时刷新
9.6 日历翻页处理
实现 onPageChanged 方法,处理日历翻页时的聚焦日期更新:
// 日历翻页回调
void onPageChanged(DateTime focusedDay) {
_focusedDay = focusedDay;
notifyListeners();
}
// 添加新事件
Future<void> addEvent(ScheduleEvent event) async {
// 提取事件的日期(仅年月日)
final day = DateTime(
event.startTime.year,
event.startTime.month,
event.startTime.day,
);
翻页与添加事件设计:
- 翻页响应:
onPageChanged仅更新聚焦日期,适配日历翻页但未选中日期的场景 - 日期归一化:添加事件时提取“年月日”作为键,确保同一日期的事件聚合
- 异步设计:
addEvent设为异步,适配后续可能的网络请求场景
9.7 事件添加逻辑
完成 addEvent 方法,实现事件的添加和状态通知:
// 添加事件到对应日期的列表
if (_events.containsKey(day)) {
_events[day]!.add(event);
} else {
_events[day] = [event];
}
// 通知组件刷新
notifyListeners();
}
事件添加的核心逻辑:
- 存在性判断:检查日期是否已有事件列表,避免覆盖已有数据
- 列表更新:已有列表则追加,无则创建新列表
- 即时通知:添加完成后通知UI,保证事件列表实时刷新
9.8 事件映射生成
实现 _generateEventsMap 方法,将排班列表转换为日期-事件映射:
// 将排班列表转换为日期-事件映射
Map<DateTime, List<ScheduleEvent>> _generateEventsMap() {
final eventsMap = <DateTime, List<ScheduleEvent>>{};
// 遍历所有排班
for (final schedule in _schedules) {
// 遍历排班中的每个事件
for (final event in schedule.events) {
// 提取事件日期(归一化)
final day = DateTime(
event.startTime.year,
event.startTime.month,
event.startTime.day,
);
映射生成逻辑:
- 双层遍历:先遍历排班,再遍历每个排班的事件,覆盖所有事件
- 日期归一化:统一提取年月日,确保不同时间的同天事件归为一组
- 空映射初始化:创建空映射表,避免操作null对象
9.9 映射生成完成
完成 _generateEventsMap 方法,构建完整的日期-事件映射:
// 将事件添加到对应日期的列表
if (eventsMap.containsKey(day)) {
eventsMap[day]!.add(event);
} else {
eventsMap[day] = [event];
}
}
}
return eventsMap;
}
映射生成的价值:
- 快速查询:将线性的排班列表转换为键值对,查询指定日期事件的时间复杂度从O(n)降为O(1)
- 数据复用:一次生成,多次查询,提升性能
- 结构统一:为日历组件提供标准化的数据源格式
9.10 模拟排班生成
实现 _generateMockSchedules 方法,生成30天的模拟排班数据:
// 生成30天的模拟排班数据
List<WorkSchedule> _generateMockSchedules() {
return List.generate(30, (index) {
// 生成近30天的日期(从今天往前推)
final date = DateTime.now().subtract(Duration(days: index));
// 生成当日事件
final events = _generateMockEvents(date);
return WorkSchedule(
id: 'SCHEDULE_${index.toString().padLeft(3, '0')}',
userId: 'user_${index % 5 + 1}',
userName: '技术员${index % 5 + 1}',
date: date,
events: events,
模拟数据生成设计:
- 时间范围:生成近30天数据,覆盖近期巡检场景
- 用户分配:通过取模分配5个不同技术员,模拟多人员排班
- ID格式化:
padLeft补零,保证ID格式统一,便于排序和识别
9.11 排班数据补全
补全 _generateMockSchedules 方法,完成排班对象的构建:
status: ScheduleStatus.published,
createdAt: date.subtract(const Duration(hours: 8)),
);
});
}
模拟排班的细节设计:
- 状态默认:所有模拟排班设为已发布状态,符合实际业务中“生效排班”的场景
- 创建时间:设为日期前8小时,模拟“提前排班”的业务逻辑
- 批量生成:通过
List.generate快速生成多组数据,提升调试效率
9.12 模拟事件生成
实现 _generateMockEvents 方法,为指定日期生成模拟事件:
// 为指定日期生成模拟事件
List<ScheduleEvent> _generateMockEvents(DateTime date) {
// 事件数量:3-6个(根据日期日数动态调整)
final eventCount = 3 + (date.day % 4);
return List.generate(eventCount, (index) {
// 事件开始时间:8点开始,每2小时一个
final hour = 8 + (index * 2);
final startTime = DateTime(
date.year,
date.month,
date.day,
hour,
0,
);
模拟事件的时间设计:
- 数量动态:
date.day % 4让不同日期的事件数量略有差异,更贴近真实场景 - 时间规律:从8点开始,每2小时一个事件,符合巡检人员的工作时间规律
- 整点开始:事件开始时间设为整点,简化模拟数据的同时贴合实际排班习惯
9.13 事件属性赋值
补全 _generateMockEvents 方法,为事件赋值所有属性:
final endTime = startTime.add(const Duration(hours: 2));
// 随机选择类型、状态、片区
final types = EventType.values;
final statuses = EventStatus.values;
final districts = ['东城区', '西城区', '朝阳区', '海淀区'];
return ScheduleEvent(
id: 'EVENT_${date.millisecondsSinceEpoch}_$index',
title: '${_getEventTypeDisplayName(types[index % types.length])} #$index',
description: '执行${_getEventTypeDisplayName(types[index % types.length])}任务',
startTime: startTime,
模拟事件的属性设计:
- 唯一ID:结合日期时间戳和索引,确保事件ID全局唯一
- 标题语义化:包含事件类型和序号,便于识别
- 时间完整:
endTime设为开始时间+2小时,模拟典型的巡检任务时长 - 片区覆盖:包含北京核心城区,贴合井盖巡检的地域管理场景
9.14 事件属性补全
完成 _generateMockEvents 方法,赋值剩余属性:
endTime: endTime,
type: types[index % types.length],
status: statuses[index % (statuses.length - 1)],
assigneeId: 'user_${index % 5 + 1}',
assigneeName: '技术员${index % 5 + 1}',
district: districts[index % districts.length],
location: '${districts[index % districts.length]}${index + 1}号位置',
metadata: {
'priority': index % 3 + 1,
'estimatedDuration': 120,
},
);
});
}
扩展属性的模拟逻辑:
- 优先级:1-3级,模拟工单的优先级管理
- 预估时长:固定120分钟,与事件的2小时时长对应
- 位置具体:片区+编号,模拟井盖的具体点位编码
- 状态随机:排除最后一个状态(避免过多取消/延期),更贴近正常业务
9.15 事件类型名称转换
实现 _getEventTypeDisplayName 方法,转换枚举为中文名称:
// 将事件类型枚举转换为中文显示名
String _getEventTypeDisplayName(EventType type) {
switch (type) {
case EventType.inspection:
return '井盖巡检';
case EventType.maintenance:
return '维护保养';
case EventType.emergency:
return '应急处理';
case EventType.meeting:
return '工作会议';
case EventType.training:
return '培训学习';
}
}
// 清空错误信息
void clearError() {
_error = null;
notifyListeners();
}
}
类型转换的价值:
- 前端友好:枚举转中文,便于UI展示,提升用户体验
- 统一管理:类型名称集中维护,便于后续修改
- 错误清理:
clearError方法支持手动清空错误,适配重试等交互场景
10. 高级日程排班组件
基于基础状态管理,实现功能更丰富的高级日程排班组件:
10.1 组件基础结构
定义 AdvancedScheduleWidget 类,初始化时加载数据:
class AdvancedScheduleWidget extends StatefulWidget {
const AdvancedScheduleWidget({super.key});
State<AdvancedScheduleWidget> createState() => _AdvancedScheduleWidgetState();
}
class _AdvancedScheduleWidgetState extends State<AdvancedScheduleWidget> {
void initState() {
super.initState();
// 页面渲染后加载数据
WidgetsBinding.instance.addPostFrameCallback((_) {
final provider = Provider.of<ScheduleProvider>(context, listen: false);
provider.loadSchedules();
});
}
组件初始化设计:
- 延迟加载:
addPostFrameCallback确保页面渲染完成后再加载数据,避免卡顿 - 非监听模式:
listen: false避免不必要的重建,提升性能 - 数据预加载:初始化时自动加载数据,无需用户手动触发
10.2 组件构建方法
实现 build 方法,基于状态渲染不同UI:
Widget build(BuildContext context) {
return Consumer<ScheduleProvider>(
builder: (context, provider, child) {
return Scaffold(
appBar: AppBar(
title: const Text('日程排班'),
actions: [
// 添加事件按钮
IconButton(
onPressed: () => _showAddEventDialog(context, provider),
icon: const Icon(Icons.add),
),
],
),
构建方法的核心逻辑:
- 状态监听:
Consumer监听ScheduleProvider状态变化,自动刷新UI - 功能扩展:AppBar添加“添加事件”按钮,支持新增工单
- 布局灵活:基于provider状态渲染加载/正常UI,适配不同场景
10.3 加载状态与布局
根据加载状态渲染不同内容,核心布局分为日历和事件区:
body: provider.loading
? const Center(child: CircularProgressIndicator())
: Column(
children: [
_buildCalendarSection(context, provider),
_buildEventsSection(context, provider),
],
),
);
},
);
}
状态驱动UI设计:
- 加载状态:展示圆形加载动画,提升用户等待体验
- 正常状态:分为日历区和事件区,保持“上日历下列表”的布局逻辑
- 错误状态:此处省略(可扩展添加错误提示UI)
10.4 日历区域构建
实现 _buildCalendarSection 方法,构建增强版日历组件:
// 构建日历区域
Widget _buildCalendarSection(BuildContext context, ScheduleProvider provider) {
return Card(
margin: const EdgeInsets.all(16),
child: TableCalendar<ScheduleEvent>(
firstDay: DateTime.utc(2020, 1, 1),
lastDay: DateTime.utc(2030, 12, 31),
focusedDay: provider.focusedDay,
selectedDayPredicate: (day) {
return isSameDay(provider.selectedDay, day);
},
高级日历的UI优化:
- 卡片包裹:
Card组件让日历区域有阴影和圆角,视觉更突出 - 间距调整:
margin: 16让日历与页面边缘保持间距,符合移动端设计规范 - 泛型支持:
TableCalendar<ScheduleEvent>明确事件类型,提升类型安全性
10.5 日历事件与交互
补充日历组件的事件加载和交互配置:
// 事件加载器:根据日期加载事件
eventLoader: (day) {
return Future.value(provider.getEventsForDay(day));
},
// 日期选中回调
onDaySelected: (selectedDay, focusedDay) {
provider.onDaySelected(selectedDay, focusedDay);
},
// 翻页回调
onPageChanged: (focusedDay) {
provider.onPageChanged(focusedDay);
},
日历交互增强:
- 异步加载:
eventLoader支持异步加载事件,适配真实的网络请求场景 - 交互解耦:回调直接调用provider方法,组件仅负责UI渲染,符合单一职责
- 翻页响应:
onPageChanged同步更新聚焦日期,保证日历视图与状态一致
10.6 日历自定义构建器
配置日历的自定义构建器,实现个性化的日期和标记样式:
// 自定义日历构建器
calendarBuilders: CalendarBuilders(
defaultBuilder: (context, day, focusedDay) {
return _buildDefaultDay(day, focusedDay, provider);
},
markerBuilder: (context, day, events) {
return _buildEventMarker(day, events);
},
),
),
);
}
自定义构建器的价值:
- 样式定制:
defaultBuilder自定义日期单元格样式,提升视觉体验 - 标记定制:
markerBuilder自定义事件标记,直观展示日期是否有事件 - 逻辑分离:将日期和标记的构建逻辑抽离为独立方法,代码更清晰
10.7 自定义日期单元格
实现 _buildDefaultDay 方法,定制日期单元格的样式:
// 自定义日期单元格样式
Widget _buildDefaultDay(DateTime day, DateTime focusedDay, ScheduleProvider provider) {
final events = provider.getEventsForDay(day);
final isToday = isSameDay(day, DateTime.now());
final isSelected = isSameDay(day, provider.selectedDay);
return Container(
margin: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: isSelected
? Theme.of(context).primaryColor.withOpacity(0.2)
: isToday
? Theme.of(context).primaryColor.withOpacity(0.1)
日期单元格样式设计:
- 状态区分:选中日期、今日、普通日期使用不同背景色,视觉层级清晰
- 间距调整:
margin: 4让单元格之间有间距,避免拥挤 - 主题适配:使用主题色,保证样式与应用整体风格统一
10.8 日期单元格样式补全
完成 _buildDefaultDay 方法,实现完整的日期单元格渲染:
: null,
border: Border.all(
color: isSelected
? Theme.of(context).primaryColor
: Colors.grey.shade300,
width: isSelected ? 2 : 1,
),
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'${day.day}',
style: TextStyle(
fontWeight: isSelected || isToday ? FontWeight.bold : FontWeight.normal,
日期单元格的视觉优化:
- 边框区分:选中日期边框加粗且使用主题色,增强选中状态
- 圆角设计:
borderRadius: 8让单元格更圆润,符合现代UI设计趋势 - 文字样式:选中/今日的日期文字加粗,提升辨识度
10.9 日期事件标记
在日期单元格中添加事件标记,直观展示是否有事件:
color: isSelected
? Theme.of(context).primaryColor
: isToday
? Theme.of(context).primaryColor
: Colors.black,
),
),
// 事件标记点
if (events.isNotEmpty)
Container(
width: 4,
height: 4,
decoration: BoxDecoration(
color: _getEventTypeColor(events.first.type),
shape: BoxShape.circle,
),
),
],
),
),
);
}
事件标记设计:
- 极简设计:4x4的圆形标记,不占用过多空间,又能清晰提示
- 颜色区分:根据事件类型使用不同颜色,直观区分事件类型
- 条件展示:仅当有事件时显示标记,避免视觉干扰
10.10 事件标记构建
实现 _buildEventMarker 方法,构建日历上的事件标记:
// 自定义事件标记
Widget _buildEventMarker(DateTime day, List<ScheduleEvent> events) {
if (events.isEmpty) return const SizedBox.shrink();
return Positioned(
bottom: 4,
right: 4,
child: Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: _getEventTypeColor(events.first.type),
shape: BoxShape.circle,
),
),
);
}
事件标记的布局设计:
- 位置固定:右下角定位,不遮挡日期数字
- 尺寸适配:6x6的圆形标记,比单元格内的标记稍大,更易识别
- 空值处理:无事件时返回空组件,避免多余渲染
10.11 事件类型颜色映射
实现 _getEventTypeColor 方法,为不同事件类型分配颜色:
// 事件类型与颜色映射
Color _getEventTypeColor(EventType type) {
switch (type) {
case EventType.inspection:
return Colors.blue; // 井盖巡检-蓝色
case EventType.maintenance:
return Colors.orange; // 维护保养-橙色
case EventType.emergency:
return Colors.red; // 应急处理-红色
case EventType.meeting:
return Colors.purple; // 工作会议-紫色
case EventType.training:
return Colors.green; // 培训学习-绿色
}
}
颜色映射的设计逻辑:
- 语义匹配:蓝色(常规)、橙色(维护)、红色(紧急)等颜色符合用户的认知习惯
- 区分度高:各颜色之间差异明显,便于快速识别事件类型
- 可扩展:新增事件类型时只需添加对应的case,便于维护
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐
所有评论(0)