在这里插入图片描述

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;
    });
  },
)

日历组件的关键配置说明:

  • 时间范围:firstDaylastDay 限定日历可操作的时间区间,避免用户选择无效日期
  • 选中判断: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 确保每个事件可被精准定位和修改
  • 时间维度:区分 startTimeendTime,适配有持续时长的巡检任务
  • 关联信息: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 控制排班的发布/归档状态
  • 时间追踪:createdAtupdatedAt 记录排班的生命周期

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();

数据加载流程:

  1. 初始化状态:加载开始时重置错误、标记加载中,通知UI展示加载动画
  2. 模拟延迟:模拟真实网络请求的耗时,贴合实际业务场景
  3. 生成数据:调用模拟方法生成排班和事件数据
  4. 状态更新:加载完成后更新状态,通知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

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐