import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:go_router/go_router.dart'; import 'package:lucide_icons/lucide_icons.dart'; import '../../../../core/di/injection.dart'; import '../../../../core/router/app_routes.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_pressable.dart'; import '../../../home/ui/navigation/home_return_policy.dart'; import '../calendar_state_manager.dart'; import '../calendar_time_utils.dart'; import '../utils/event_color_resolver.dart'; import '../widgets/bottom_dock.dart'; import '../../data/models/schedule_item_model.dart'; import '../../data/services/calendar_repository.dart'; class CalendarMonthScreen extends StatefulWidget { final bool resetToToday; const CalendarMonthScreen({super.key, this.resetToToday = false}); @override State createState() => _CalendarMonthScreenState(); } class _CalendarMonthScreenState extends State with WidgetsBindingObserver { late final CalendarStateManager _calendarManager; late DateTime _currentMonth; late DateTime _selectedDate; final Map> _eventsByDay = {}; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _calendarManager = sl(); if (widget.resetToToday) { _calendarManager.resetToToday(); } final savedDate = _calendarManager.selectedDate; _selectedDate = savedDate; _currentMonth = DateTime(savedDate.year, savedDate.month, 1); _loadMonthEvents(); } Future _loadMonthEvents({bool forceRefresh = false}) async { final events = await sl().getMonthEvents( _currentMonth, forceRefresh: forceRefresh, ); if (!mounted) { return; } _eventsByDay.clear(); for (final event in events) { final key = formatYmd(event.startAt); _eventsByDay[key] = [...(_eventsByDay[key] ?? const []), event]; } setState(() {}); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { _loadMonthEvents(forceRefresh: true); } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.todoBg, body: PopScope( canPop: false, onPopInvokedWithResult: (didPop, result) { if (!didPop) { returnToHomePreserveState(context, forceGoHome: true); } }, child: SafeArea( child: Column( children: [ _buildHeader(), Expanded( child: SingleChildScrollView( child: Padding( padding: const EdgeInsets.only(bottom: 84), child: _buildMonthContent(), ), ), ), _buildBottomDock(), ], ), ), ), ); } Widget _buildHeader() { return SizedBox( height: 76, child: Padding( padding: const EdgeInsets.only(left: 16, right: 16, top: 14, bottom: 6), child: Column( children: [ SizedBox( height: 56, child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ AppPressable( borderRadius: BorderRadius.circular(AppRadius.md), onTap: _showMonthPicker, child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ AnimatedSwitcher( duration: const Duration(milliseconds: 160), switchInCurve: Curves.easeOut, switchOutCurve: Curves.easeOut, child: Text( '${_currentMonth.month}月', key: ValueKey(_currentMonth.month), style: const TextStyle( fontSize: 22, fontWeight: FontWeight.w700, color: AppColors.slate900, ), ), ), const SizedBox(width: 6), const Icon( LucideIcons.chevronDown, size: 16, color: AppColors.slate900, ), ], ), ), const Spacer(), AppPressable( borderRadius: BorderRadius.circular(AppRadius.full), onTap: () => context.push(AppRoutes.calendarEventCreate), child: Container( width: 36, height: 36, decoration: BoxDecoration( color: AppColors.blue600, borderRadius: BorderRadius.circular(AppRadius.full), ), child: const Icon( LucideIcons.plus, size: 20, color: AppColors.white, ), ), ), ], ), ), ], ), ), ); } Widget _buildMonthContent() { return Column( children: [ _buildWeekdayHeader(), Container(height: 1, color: AppColors.border), ..._buildWeeks(), ], ); } Widget _buildWeekdayHeader() { const weekdays = ['日', '一', '二', '三', '四', '五', '六']; return SizedBox( height: 40, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: weekdays .map( (day) => SizedBox( width: 36, child: Center( child: Text( day, style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: AppColors.slate400, ), ), ), ), ) .toList(), ), ), ); } List _buildWeeks() { final firstDayOfMonth = DateTime( _currentMonth.year, _currentMonth.month, 1, ); final lastDayOfMonth = DateTime( _currentMonth.year, _currentMonth.month + 1, 0, ); final startWeekday = firstDayOfMonth.weekday % 7; final daysInMonth = lastDayOfMonth.day; final totalCells = ((daysInMonth + startWeekday) / 7).ceil() * 7; final weeks = []; for (var weekStart = 0; weekStart < totalCells; weekStart += 7) { weeks.add(_buildWeekRow(weekStart, startWeekday, daysInMonth)); if (weekStart + 7 < totalCells) { weeks.add(Container(height: 1, color: AppColors.border)); } } return weeks; } Widget _buildWeekRow(int weekStart, int startWeekday, int daysInMonth) { return Padding( padding: const EdgeInsets.only(left: 16, right: 16, top: 14, bottom: 0), child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: List.generate(7, (index) { final dayIndex = weekStart + index - startWeekday + 1; if (dayIndex < 1 || dayIndex > daysInMonth) { return SizedBox(width: 36, height: 36); } final date = DateTime( _currentMonth.year, _currentMonth.month, dayIndex, ); final isSelected = isSameDay(_selectedDate, date); return AppPressable( borderRadius: BorderRadius.circular(AppRadius.full), onTap: () { setState(() { _selectedDate = date; }); _calendarManager.setSelectedDate(date); _calendarManager.setViewType(CalendarViewType.month); context.push( '${AppRoutes.calendarDayWeek}?date=${formatYmd(date)}', ); }, child: AnimatedContainer( duration: const Duration(milliseconds: 140), curve: Curves.easeOut, width: 36, height: 36, decoration: BoxDecoration( color: isSelected ? AppColors.blue100 : Colors.transparent, borderRadius: BorderRadius.circular(AppRadius.full), ), child: Center( child: Text( '$dayIndex', style: TextStyle( fontSize: 15, fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, color: isSelected ? AppColors.blue600 : AppColors.slate900, ), ), ), ), ); }), ), const SizedBox(height: 10), _buildWeekEvents(weekStart, startWeekday, daysInMonth), ], ), ); } 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 = _eventsByDay[formatYmd(date)] ?? const []; final displayEvents = events.take(2).toList(); final remainingCount = events.length - 2; return SizedBox( width: 38, height: 70, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ...displayEvents.map((event) { final color = resolveEventColor( status: event.status, colorHex: event.metadata?.color, ); return AppPressable( borderRadius: BorderRadius.circular(AppRadius.sm), onTap: () { _calendarManager.setSelectedDate(date); context.push('/calendar/events/${event.id}'); }, child: Container( margin: const EdgeInsets.only(bottom: 2), padding: const EdgeInsets.symmetric( horizontal: 4, vertical: 2, ), decoration: BoxDecoration( color: color.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(4), ), child: Text( event.title, style: TextStyle( fontSize: 9, color: color, fontWeight: FontWeight.w500, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ); }), if (remainingCount > 0) AppPressable( borderRadius: BorderRadius.circular(AppRadius.sm), onTap: () { _calendarManager.setSelectedDate(date); _calendarManager.setViewType(CalendarViewType.day); context.push( '${AppRoutes.calendarDayWeek}?date=${formatYmd(date)}', ); }, child: Text( '+$remainingCount', style: const TextStyle( fontSize: 9, color: AppColors.slate500, fontWeight: FontWeight.w500, ), ), ), ], ), ); }), ), ); } void _showMonthPicker() { var selectedYear = _currentMonth.year; var selectedMonth = _currentMonth.month; showModalBottomSheet( context: context, backgroundColor: AppColors.white, builder: (context) { return StatefulBuilder( builder: (context, setSheetState) { return SizedBox( height: 300, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('取消'), ), TextButton( onPressed: () { Navigator.pop(context); setState(() { _currentMonth = DateTime( selectedYear, selectedMonth, 1, ); _selectedDate = DateTime( selectedYear, selectedMonth, 1, ); }); _calendarManager.setSelectedDate(_selectedDate); _loadMonthEvents(); }, child: const Text('确定'), ), ], ), Expanded( child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: CupertinoPicker( itemExtent: 40, scrollController: FixedExtentScrollController( initialItem: _currentMonth.year - 2020, ), onSelectedItemChanged: (index) { setSheetState(() { selectedYear = 2020 + index; }); }, children: List.generate(20, (index) { return Center(child: Text('${2020 + index}年')); }), ), ), Expanded( child: CupertinoPicker( itemExtent: 40, scrollController: FixedExtentScrollController( initialItem: _currentMonth.month - 1, ), onSelectedItemChanged: (index) { setSheetState(() { selectedMonth = index + 1; }); }, children: List.generate(12, (index) { return Center(child: Text('${index + 1}月')); }), ), ), ], ), ), ], ), ); }, ); }, ); } Widget _buildBottomDock() { return BottomDock( activeTab: DockTab.calendar, onTodoTap: () { _calendarManager.setViewType(CalendarViewType.month); context.push(AppRoutes.todoList); }, onCalendarTap: () {}, onHomeTap: () => returnToHomePreserveState(context, forceGoHome: true), ); } }