Files
social-app/docs/plans/2026-03-02-calendar-create-event-plan.md
T

31 KiB
Raw Blame History

日历事件创建功能实现计划

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: 在日历月视图和日视图右上角添加 + 号图标,点击后弹出底部表单创建日历事件,创建完成后在视图中正确显示,并可点击进入详情页查看和编辑。

Architecture:

  • 使用 Mock Service 模式存储日历事件(参考 mock_history_service.dart
  • 底部弹窗表单使用 showModalBottomSheet
  • 日视图使用时间线布局显示事件

Tech Stack: Flutter, BLoC, go_router, lucide_icons


Task 1: 创建日历事件数据模型

Files:

  • Create: apps/lib/features/calendar/data/models/schedule_item_model.dart

Step 1: 创建数据模型文件

import 'package:flutter/material.dart';

enum ScheduleSourceType { manual, imported, agentGenerated }

enum ScheduleStatus { active, completed, canceled, archived }

class ScheduleItemModel {
  final String id;
  final String title;
  final String? description;
  final DateTime startAt;
  final DateTime? endAt;
  final String timezone;
  final ScheduleMetadata? metadata;
  final ScheduleSourceType sourceType;
  final ScheduleStatus status;
  final DateTime createdAt;

  ScheduleItemModel({
    required this.id,
    required this.title,
    this.description,
    required this.startAt,
    this.endAt,
    this.timezone = 'Asia/Shanghai',
    this.metadata,
    this.sourceType = ScheduleSourceType.manual,
    this.status = ScheduleStatus.active,
    DateTime? createdAt,
  }) : createdAt = createdAt ?? DateTime.now();

  ScheduleItemModel copyWith({
    String? id,
    String? title,
    String? description,
    DateTime? startAt,
    DateTime? endAt,
    String? timezone,
    ScheduleMetadata? metadata,
    ScheduleSourceType? sourceType,
    ScheduleStatus? status,
    DateTime? createdAt,
  }) {
    return ScheduleItemModel(
      id: id ?? this.id,
      title: title ?? this.title,
      description: description ?? this.description,
      startAt: startAt ?? this.startAt,
      endAt: endAt ?? this.endAt,
      timezone: timezone ?? this.timezone,
      metadata: metadata ?? this.metadata,
      sourceType: sourceType ?? this.sourceType,
      status: status ?? this.status,
      createdAt: createdAt ?? this.createdAt,
    );
  }
}

class ScheduleMetadata {
  final String? color;
  final String? location;
  final String? notes;
  final List<Attachment>? attachments;

  ScheduleMetadata({
    this.color,
    this.location,
    this.notes,
    this.attachments,
  });

  ScheduleMetadata copyWith({
    String? color,
    String? location,
    String? notes,
    List<Attachment>? attachments,
  }) {
    return ScheduleMetadata(
      color: color ?? this.color,
      location: location ?? this.location,
      notes: notes ?? this.notes,
      attachments: attachments ?? this.attachments,
    );
  }
}

class Attachment {
  final String name;
  final String? url;
  final String? content;
  final String type;

  Attachment({
    required this.name,
    this.url,
    this.content,
    this.type = 'document',
  });
}

const defaultColors = [
  Color(0xFF3B82F6), // 蓝色
  Color(0xFF8B5CF6), // 紫色
  Color(0xFF10B981), // 绿色
  Color(0xFFF59E0B), // 黄色
  Color(0xFFEF4444), // 红色
];

Step 2: 提交

git add apps/lib/features/calendar/data/models/schedule_item_model.dart
git commit -m "feat(calendar): 添加日历事件数据模型"

Task 2: 创建 Mock Calendar Service

Files:

  • Create: apps/lib/features/calendar/data/services/mock_calendar_service.dart

Step 1: 创建 Mock Service

import 'package:Env/Env.dart';
import '../models/schedule_item_model.dart';

class MockCalendarService {
  static final MockCalendarService _instance = MockCalendarService._internal();
  factory MockCalendarService() => _instance;
  MockCalendarService._internal();

  final List<ScheduleItemModel> _events = [];

  List<ScheduleItemModel> get events => List.unmodifiable(_events);

  List<ScheduleItemModel> getEventsForDay(DateTime date) {
    final dateOnly = DateTime(date.year, date.month, date.day);
    return _events.where((event) {
      final eventDate = DateTime(
        event.startAt.year,
        event.startAt.month,
        event.startAt.day,
      );
      return eventDate == dateOnly && event.status == ScheduleStatus.active;
    }).toList()
      ..sort((a, b) => a.startAt.compareTo(b.startAt));
  }

  List<ScheduleItemModel> getEventsForRange(DateTime start, DateTime end) {
    return _events.where((event) {
      return event.startAt.isAfter(start.subtract(const Duration(days: 1))) &&
          event.startAt.isBefore(end.add(const Duration(days: 1))) &&
          event.status == ScheduleStatus.active;
    }).toList()
      ..sort((a, b) => a.startAt.compareTo(b.startAt));
  }

  ScheduleItemModel? getEventById(String id) {
    try {
      return _events.firstWhere((e) => e.id == id);
    } catch (_) {
      return null;
    }
  }

  void addEvent(ScheduleItemModel event) {
    _events.add(event);
  }

  void updateEvent(ScheduleItemModel event) {
    final index = _events.indexWhere((e) => e.id == event.id);
    if (index >= 0) {
      _events[index] = event;
    }
  }

  void deleteEvent(String id) {
    _events.removeWhere((e) => e.id == id);
  }
}

class CalendarService {
  static final CalendarService _instance = CalendarService._internal();
  factory CalendarService() => _instance;
  CalendarService._internal();

  MockCalendarService get _mock => MockCalendarService();

  List<ScheduleItemModel> getEventsForDay(DateTime date) {
    if (Env.isMockApi) {
      return _mock.getEventsForDay(date);
    }
    throw UnimplementedError('Real API not implemented');
  }

  List<ScheduleItemModel> getEventsForRange(DateTime start, DateTime end) {
    if (Env.isMockApi) {
      return _mock.getEventsForRange(start, end);
    }
    throw UnimplementedError('Real API not implemented');
  }

  ScheduleItemModel? getEventById(String id) {
    if (Env.isMockApi) {
      return _mock.getEventById(id);
    }
    throw UnimplementedError('Real API not implemented');
  }

  void addEvent(ScheduleItemModel event) {
    if (Env.isMockApi) {
      _mock.addEvent(event);
      return;
    }
    throw UnimplementedError('Real API not implemented');
  }

  void updateEvent(ScheduleItemModel event) {
    if (Env.isMockApi) {
      _mock.updateEvent(event);
      return;
    }
    throw UnimplementedError('Real API not implemented');
  }

  void deleteEvent(String id) {
    if (Env.isMockApi) {
      _mock.deleteEvent(id);
      return;
    }
    throw UnimplementedError('Real API not implemented');
  }
}

Step 2: 提交

git add apps/lib/features/calendar/data/services/mock_calendar_service.dart
git commit -m "feat(calendar): 添加 Mock Calendar Service"

Task 3: 创建日历事件创建底部弹窗

Files:

  • Create: apps/lib/features/calendar/ui/widgets/create_event_sheet.dart

Step 1: 创建底部弹窗组件

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import '../../../../core/theme/design_tokens.dart';
import '../../data/models/schedule_item_model.dart';
import '../../data/services/mock_calendar_service.dart';

class CreateEventSheet extends StatefulWidget {
  final DateTime? initialDate;
  final ScheduleItemModel? editingEvent;

  const CreateEventSheet({
    super.key,
    this.initialDate,
    this.editingEvent,
  });

  static Future<void> show(BuildContext context, {DateTime? initialDate}) {
    return showModalBottomSheet(
      context: context,
      isScrollControlled: true,
      backgroundColor: Colors.transparent,
      builder: (context) => CreateEventSheet(initialDate: initialDate),
    );
  }

  static Future<void> edit(BuildContext context, ScheduleItemModel event) {
    return showModalBottomSheet(
      context: context,
      isScrollControlled: true,
      backgroundColor: Colors.transparent,
      builder: (context) => CreateEventSheet(editingEvent: event),
    );
  }

  @override
  State<CreateEventSheet> createState() => _CreateEventSheetState();
}

class _CreateEventSheetState extends State<CreateEventSheet> {
  late TabController _tabController;
  final _titleController = TextEditingController();
  final _descriptionController = TextEditingController();
  final _locationController = TextEditingController();
  final _notesController = TextEditingController();

  late DateTime _startDate;
  late DateTime _startTime;
  DateTime? _endDate;
  DateTime? _endTime;
  String _selectedColor = '#3B82F6';

  bool get _isEditing => widget.editingEvent != null;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 2, vsync: this);

    if (_isEditing) {
      final event = widget.editingEvent!;
      _titleController.text = event.title;
      _descriptionController.text = event.description ?? '';
      _locationController.text = event.metadata?.location ?? '';
      _notesController.text = event.metadata?.notes ?? '';
      _startDate = event.startAt;
      _startTime = event.startAt;
      _endDate = event.endAt;
      _endTime = event.endAt;
      _selectedColor = event.metadata?.color ?? '#3B82F6';
    } else {
      final now = widget.initialDate ?? DateTime.now();
      _startDate = now;
      _startTime = now;
      _endDate = now;
      _endTime = now.add(const Duration(hours: 1));
    }
  }

  @override
  void dispose() {
    _tabController.dispose();
    _titleController.dispose();
    _descriptionController.dispose();
    _locationController.dispose();
    _notesController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      height: MediaQuery.of(context).size.height * 0.85,
      decoration: const BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
      ),
      child: Column(
        children: [
          _buildHeader(),
          _buildTabBar(),
          Expanded(child: _buildTabContent()),
        ],
      ),
    );
  }

  Widget _buildHeader() {
    return Container(
      height: 56,
      padding: const EdgeInsets.symmetric(horizontal: 16),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          GestureDetector(
            onTap: () => Navigator.pop(context),
            child: const Icon(LucideIcons.x, size: 24, color: AppColors.slate700),
          ),
          Text(
            _isEditing ? '编辑日程' : '新建日程',
            style: const TextStyle(
              fontSize: 17,
              fontWeight: FontWeight.w600,
              color: AppColors.slate900,
            ),
          ),
          GestureDetector(
            onTap: _saveEvent,
            child: Text(
              '保存',
              style: TextStyle(
                fontSize: 17,
                fontWeight: FontWeight.w600,
                color: _titleController.text.trim().isNotEmpty
                    ? AppColors.blue600
                    : AppColors.slate400,
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildTabBar() {
    return Container(
      decoration: const BoxDecoration(
        border: Border(bottom: BorderSide(color: AppColors.border)),
      ),
      child: TabBar(
        controller: _tabController,
        labelColor: AppColors.blue600,
        unselectedLabelColor: AppColors.slate600,
        indicatorColor: AppColors.blue600,
        tabs: const [
          Tab(text: '基础'),
          Tab(text: '进阶'),
        ],
      ),
    );
  }

  Widget _buildTabContent() {
    return TabBarView(
      controller: _tabController,
      children: [
        _buildBasicTab(),
        _buildAdvancedTab(),
      ],
    );
  }

  Widget _buildBasicTab() {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          _buildTextField('标题', _titleController, '请输入日程标题'),
          const SizedBox(height: 20),
          _buildDateTimePicker('开始', _startDate, _startTime, (date, time) {
            setState(() {
              _startDate = date;
              _startTime = time;
            });
          }),
          const SizedBox(height: 20),
          _buildDateTimePicker('结束', _endDate ?? _startDate, _endTime ?? _startTime, (date, time) {
            setState(() {
              _endDate = date;
              _endTime = time;
            });
          }, isOptional: true),
        ],
      ),
    );
  }

  Widget _buildAdvancedTab() {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          _buildTextField('描述', _descriptionController, '请输入描述'),
          const SizedBox(height: 20),
          _buildTextField('地点', _locationController, '请输入地点'),
          const SizedBox(height: 20),
          _buildColorPicker(),
          const SizedBox(height: 20),
          _buildTextField('备注', _notesController, '请输入备注', maxLines: 3),
        ],
      ),
    );
  }

  Widget _buildTextField(String label, TextEditingController controller, String hint, {int maxLines = 1}) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          label,
          style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.slate700),
        ),
        const SizedBox(height: 8),
        TextField(
          controller: controller,
          maxLines: maxLines,
          decoration: InputDecoration(
            hintText: hint,
            hintStyle: const TextStyle(color: AppColors.slate400),
            filled: true,
            fillColor: AppColors.slate50,
            border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none),
            contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
          ),
        ),
      ],
    );
  }

  Widget _buildDateTimePicker(String label, DateTime date, DateTime time, Function(DateTime, DateTime) onChanged, {bool isOptional = false}) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          label + (isOptional ? '(可选)' : ''),
          style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.slate700),
        ),
        const SizedBox(height: 8),
        Row(
          children: [
            Expanded(
              child: GestureDetector(
                onTap: () => _showDatePicker(date, (newDate) {
                  onChanged(newDate, time);
                }),
                child: Container(
                  padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
                  decoration: BoxDecoration(
                    color: AppColors.slate50,
                    borderRadius: BorderRadius.circular(10),
                  ),
                  child: Text(
                    '${date.year}${date.month}${date.day}日',
                    style: const TextStyle(fontSize: 15, color: AppColors.slate900),
                  ),
                ),
              ),
            ),
            const SizedBox(width: 12),
            Expanded(
              child: GestureDetector(
                onTap: () => _showTimePicker(time, (newTime) {
                  onChanged(date, newTime);
                }),
                child: Container(
                  padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
                  decoration: BoxDecoration(
                    color: AppColors.slate50,
                    borderRadius: BorderRadius.circular(10),
                  ),
                  child: Text(
                    '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}',
                    style: const TextStyle(fontSize: 15, color: AppColors.slate900),
                  ),
                ),
              ),
            ),
          ],
        ),
      ],
    );
  }

  Widget _buildColorPicker() {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        const Text(
          '颜色',
          style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.slate700),
        ),
        const SizedBox(height: 8),
        Row(
          children: defaultColors.map((color) {
            final colorHex = '#${color.value.toRadixString(16).substring(2).toUpperCase()}';
            final isSelected = _selectedColor == colorHex;
            return GestureDetector(
              onTap: () => setState(() => _selectedColor = colorHex),
              child: Container(
                margin: const EdgeInsets.only(right: 12),
                width: 32,
                height: 32,
                decoration: BoxDecoration(
                  color: color,
                  shape: BoxShape.circle,
                  border: isSelected ? Border.all(color: AppColors.slate900, width: 2) : null,
                ),
                child: isSelected ? const Icon(Icons.check, size: 16, color: Colors.white) : null,
              ),
            );
          }).toList(),
        ),
      ],
    );
  }

  void _showDatePicker(DateTime initial, Function(DateTime) onChanged) {
    showModalBottomSheet(
      context: context,
      builder: (context) => Container(
        height: 280,
        color: Colors.white,
        child: Column(
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                TextButton(onPressed: () => Navigator.pop(context), child: const Text('取消')),
                TextButton(onPressed: () => Navigator.pop(context), child: const Text('确定')),
              ],
            ),
            Expanded(
              child: CupertinoDatePicker(
                mode: CupertinoDatePickerMode.date,
                initialDateTime: initial,
                onDateTimeChanged: onChanged,
              ),
            ),
          ],
        ),
      ),
    );
  }

  void _showTimePicker(DateTime initial, Function(DateTime) onChanged) {
    showModalBottomSheet(
      context: context,
      builder: (context) => Container(
        height: 280,
        color: Colors.white,
        child: Column(
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                TextButton(onPressed: () => Navigator.pop(context), child: const Text('取消')),
                TextButton(onPressed: () => Navigator.pop(context), child: const Text('确定')),
              ],
            ),
            Expanded(
              child: CupertinoDatePicker(
                mode: CupertinoDatePickerMode.time,
                initialDateTime: initial,
                onDateTimeChanged: onChanged,
              ),
            ),
          ],
        ),
      ),
    );
  }

  void _saveEvent() {
    if (_titleController.text.trim().isEmpty) return;

    final startAt = DateTime(
      _startDate.year,
      _startDate.month,
      _startDate.day,
      _startTime.hour,
      _startTime.minute,
    );

    DateTime? endAt;
    if (_endDate != null && _endTime != null) {
      endAt = DateTime(
        _endDate!.year,
        _endDate!.month,
        _endDate!.day,
        _endTime!.hour,
        _endTime!.minute,
      );
    }

    final metadata = ScheduleMetadata(
      color: _selectedColor,
      location: _locationController.text.trim().isNotEmpty ? _locationController.text.trim() : null,
      notes: _notesController.text.trim().isNotEmpty ? _notesController.text.trim() : null,
    );

    final event = ScheduleItemModel(
      id: _isEditing ? widget.editingEvent!.id : 'evt_${DateTime.now().millisecondsSinceEpoch}',
      title: _titleController.text.trim(),
      description: _descriptionController.text.trim().isNotEmpty ? _descriptionController.text.trim() : null,
      startAt: startAt,
      endAt: endAt,
      metadata: metadata,
    );

    final service = CalendarService();
    if (_isEditing) {
      service.updateEvent(event);
    } else {
      service.addEvent(event);
    }

    Navigator.pop(context);
  }
}

Step 2: 提交

git add apps/lib/features/calendar/ui/widgets/create_event_sheet.dart
git commit -m "feat(calendar): 添加日历事件创建底部弹窗组件"

Task 4: 在月视图添加 + 号图标

Files:

  • Modify: apps/lib/features/calendar/ui/screens/calendar_month_screen.dart

Step 1: 添加 + 号图标到 header

_buildHeader 方法中,在 Row 的末尾添加:

// 在现有的 Row children 中添加
const Spacer(),
GestureDetector(
  onTap: () => CreateEventSheet.show(context),
  child: Container(
    width: 36,
    height: 36,
    decoration: BoxDecoration(
      color: AppColors.blue600,
      borderRadius: BorderRadius.circular(18),
    ),
    child: const Icon(
      LucideIcons.plus,
      size: 20,
      color: Colors.white,
    ),
  ),
),

同时添加 import

import '../../ui/widgets/create_event_sheet.dart';

Step 2: 提交

git add apps/lib/features/calendar/ui/screens/calendar_month_screen.dart
git commit -m "feat(calendar): 在月视图添加创建事件按钮"

Task 5: 在日视图添加 + 号图标

Files:

  • Modify: apps/lib/features/calendar/ui/screens/calendar_dayweek_screen.dart

Step 1: 添加 + 号图标到 header

_buildHeader 方法中,在 Row 的末尾添加:

const Spacer(),
GestureDetector(
  onTap: () => CreateEventSheet.show(context, initialDate: _selectedDate),
  child: Container(
    width: 36,
    height: 36,
    decoration: BoxDecoration(
      color: AppColors.blue600,
      borderRadius: BorderRadius.circular(18),
    ),
    child: const Icon(
      LucideIcons.plus,
      size: 20,
      color: Colors.white,
    ),
  ),
),

同时添加 import

import '../../ui/widgets/create_event_sheet.dart';

Step 2: 提交

git add apps/lib/features/calendar/ui/screens/calendar_dayweek_screen.dart
git commit -m "feat(calendar): 在日视图添加创建事件按钮"

Task 6: 在月视图显示事件

Files:

  • Modify: apps/lib/features/calendar/ui/screens/calendar_month_screen.dart

Step 1: 修改 _buildWeekEvents 方法显示事件

import '../../data/services/mock_calendar_service.dart';

Widget _buildWeekEvents(int weekStart, int startWeekday, int daysInMonth) {
  // 找到这一周第一天的日期
  final firstDayOfMonth = DateTime(_currentMonth.year, _currentMonth.month, 1);
  final weekFirstDate = firstDayOfMonth.add(Duration(days: weekStart - startWeekday));

  return SizedBox(
    height: 70,
    child: Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: List.generate(7, (index) {
        final dayIndex = weekStart + index - startWeekday + 1;
        if (dayIndex < 1 || dayIndex > daysInMonth) {
          return const SizedBox(width: 38, height: 1);
        }
        
        final date = weekFirstDate.add(Duration(days: index));
        final events = CalendarService().getEventsForDay(date);
        
        return SizedBox(
          width: 38,
          height: 70,
          child: Column(
            children: events.take(2).map((event) {
              final color = _parseColor(event.metadata?.color);
              return Container(
                margin: const EdgeInsets.only(bottom: 2),
                padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
                decoration: BoxDecoration(
                  color: color.withOpacity(0.2),
                  borderRadius: BorderRadius.circular(4),
                ),
                child: Text(
                  event.title,
                  style: TextStyle(fontSize: 9, color: color, fontWeight: FontWeight.w500),
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                ),
              );
            }).toList(),
          ),
        );
      }),
    ),
  );
}

Color _parseColor(String? hex) {
  if (hex == null || hex.isEmpty) return AppColors.blue600;
  try {
    return Color(int.parse(hex.replaceFirst('#', '0xFF')));
  } catch (_) {
    return AppColors.blue600;
  }
}

Step 2: 提交

git add apps/lib/features/calendar/ui/screens/calendar_month_screen.dart
git commit -m "feat(calendar): 在月视图显示事件"

Task 7: 在日视图显示事件

Files:

  • Modify: apps/lib/features/calendar/ui/screens/calendar_dayweek_screen.dart

Step 1: 修改 _buildTimelineBoard 方法显示事件

import '../../data/services/mock_calendar_service.dart';
import '../../data/models/schedule_item_model.dart';

Widget _buildTimelineBoard() {
  final now = DateTime.now();
  final showCurrent = shouldShowCurrentMarker(_selectedDate, now);
  final events = CalendarService().getEventsForDay(_selectedDate);
  final rows = <Widget>[];

  for (var hour = 7; hour <= 22; hour++) {
    // 查找这个小时的事件
    final hourEvents = events.where((e) => e.startAt.hour == hour).toList();
    
    if (hourEvents.isNotEmpty) {
      rows.add(_buildEventRow(hourEvents));
    }
    
    rows.add(_buildTimelineRow(formatHour(hour)));
    if (showCurrent && now.hour == hour) {
      rows.add(_buildTimelineRow(formatHm(now), isCurrentTime: true));
    }
  }

  rows.add(_buildTimelineRow(formatHour(24), isDisabled: true));
  return Column(children: rows);
}

Widget _buildEventRow(List<ScheduleItemModel> events) {
  return SizedBox(
    height: 34,
    child: Row(
      children: [
        const SizedBox(width: 52),
        Expanded(
          child: ...events.map((event) => GestureDetector(
            onTap: () => context.push('/calendar/events/${event.id}'),
            child: Container(
              margin: const EdgeInsets.only(bottom: 2),
              padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
              decoration: BoxDecoration(
                color: _parseColor(event.metadata?.color).withOpacity(0.2),
                borderRadius: BorderRadius.circular(6),
                border: Border.all(
                  color: _parseColor(event.metadata?.color),
                  width: 1,
                ),
              ),
              child: Row(
                children: [
                  Container(
                    width: 8,
                    height: 8,
                    decoration: BoxDecoration(
                      color: _parseColor(event.metadata?.color),
                      shape: BoxShape.circle,
                    ),
                  ),
                  const SizedBox(width: 6),
                  Expanded(
                    child: Text(
                      event.title,
                      style: TextStyle(
                        fontSize: 12,
                        fontWeight: FontWeight.w500,
                        color: _parseColor(event.metadata?.color),
                      ),
                      maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                    ),
                  ),
                ],
              ),
            ),
          )),
        ),
      ],
    ),
  );
}

Color _parseColor(String? hex) {
  if (hex == null || hex.isEmpty) return AppColors.blue600;
  try {
    return Color(int.parse(hex.replaceFirst('#', '0xFF')));
  } catch (_) {
    return AppColors.blue600;
  }
}

Step 2: 提交

git add apps/lib/features/calendar/ui/screens/calendar_dayweek_screen.dart
git commit -m "feat(calendar): 在日视图显示事件"

Task 8: 更新事件详情页支持编辑和删除

Files:

  • Modify: apps/lib/features/calendar/ui/screens/calendar_event_detail_screen.dart

Step 1: 修改详情页接收事件 ID 并加载数据

import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:go_router/go_router.dart';
import '../../../../core/theme/design_tokens.dart';
import '../../data/services/mock_calendar_service.dart';
import '../../data/models/schedule_item_model.dart';
import '../widgets/create_event_sheet.dart';

class CalendarEventDetailScreen extends StatefulWidget {
  final String eventId;

  const CalendarEventDetailScreen({super.key, required this.eventId});

  @override
  State<CalendarEventDetailScreen> createState() => _CalendarEventDetailScreenState();
}

class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
  ScheduleItemModel? _event;

  @override
  void initState() {
    super.initState();
    _loadEvent();
  }

  void _loadEvent() {
    _event = CalendarService().getEventById(widget.eventId);
  }

  @override
  Widget build(BuildContext context) {
    if (_event == null) {
      return Scaffold(
        body: Center(child: Text('Event not found', style: TextStyle(color: AppColors.slate600))),
      );
    }
    // ... 其余代码使用 _event 而不是硬编码数据
  }
}

Step 2: 修改 router 传递 eventId

// in app_router.dart
GoRoute(
  path: '/calendar/events/:id',
  builder: (context, state) => CalendarEventDetailScreen(
    eventId: state.pathParameters['id']!,
  ),
),

Step 3: 添加编辑和删除功能

在详情页的编辑和删除按钮上添加:

// 编辑按钮
GestureDetector(
  onTap: () => CreateEventSheet.edit(context, _event!),
  child: Container(...),
),

// 删除按钮
GestureDetector(
  onTap: () => _showDeleteConfirmation(),
  child: Container(...),
),

void _showDeleteConfirmation() {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('删除日程'),
      content: const Text('确定要删除这个日程吗?'),
      actions: [
        TextButton(onPressed: () => Navigator.pop(context), child: const Text('取消')),
        TextButton(
          onPressed: () {
            CalendarService().deleteEvent(widget.eventId);
            Navigator.pop(context);
            context.pop();
          },
          child: Text('删除', style: TextStyle(color: AppColors.red500)),
        ),
      ],
    ),
  );
}

Step 4: 提交

git add apps/lib/features/calendar/ui/screens/calendar_event_detail_screen.dart
git add apps/lib/core/router/app_router.dart
git commit -m "feat(calendar): 详情页支持编辑和删除事件"

Task 9: 运行测试并验证

Step 1: 运行 Flutter 测试

cd apps
flutter test

Step 2: 验证构建

flutter build apk --debug

Step 3: 提交

git commit -m "chore(calendar): 测试通过并验证构建"

执行选项

Plan complete and saved to docs/plans/2026-03-02-calendar-create-event-design.md. Two execution options:

1. Subagent-Driven (this session) - I dispatch fresh subagent per task, review between tasks, fast iteration

2. Parallel Session (separate) - Open new session with executing-plans, batch execution with checkpoints

Which approach?