import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:lucide_icons/lucide_icons.dart'; import '../../../../core/di/injection.dart'; import '../../../../core/notifications/local_notification_service.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_loading_indicator.dart'; import '../../../../shared/widgets/app_sheet_input_field.dart'; import '../../../../shared/widgets/toast/toast.dart'; import '../../../../shared/widgets/toast/toast_type.dart'; import '../../data/models/schedule_item_model.dart'; import '../../data/services/calendar_service.dart'; class CreateEventSheet extends StatefulWidget { final DateTime? initialDate; final ScheduleItemModel? editingEvent; final VoidCallback? onSaved; const CreateEventSheet({ super.key, this.initialDate, this.editingEvent, this.onSaved, }); static Future show( BuildContext context, { DateTime? initialDate, VoidCallback? onSaved, }) { return showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) => CreateEventSheet(initialDate: initialDate, onSaved: onSaved), ); } static Future edit( BuildContext context, ScheduleItemModel event, { VoidCallback? onSaved, }) { return showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) => CreateEventSheet(editingEvent: event, onSaved: onSaved), ); } @override State createState() => _CreateEventSheetState(); } class _CreateEventSheetState extends State with SingleTickerProviderStateMixin { static const List _defaultReminderOptions = [ null, 0, 5, 10, 15, 30, 60, 120, ]; 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'; int? _reminderMinutes = 15; bool _saving = false; 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'; _reminderMinutes = _sanitizeReminderMinutes( event.metadata?.reminderMinutes, ); } else { final now = widget.initialDate ?? _roundToNearestMinute(DateTime.now(), 5); _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(); } DateTime _roundToNearestMinute(DateTime dt, int interval) { final totalMinutes = dt.hour * 60 + dt.minute; final rounded = ((totalMinutes / interval).round() * interval); final hours = rounded ~/ 60; final minutes = rounded % 60; final dayOffset = hours >= 24 ? 1 : 0; final newHour = hours % 24; return DateTime(dt.year, dt.month, dt.day + dayOffset, newHour, minutes); } @override Widget build(BuildContext context) { return AnimatedPadding( duration: const Duration(milliseconds: 150), curve: Curves.easeOut, padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, ), child: Container( height: MediaQuery.of(context).size.height * 0.85, decoration: const BoxDecoration( color: AppColors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: AppSpacing.sm), Center( child: Container( width: AppSpacing.xl + AppSpacing.sm, height: AppSpacing.xs, decoration: BoxDecoration( color: AppColors.slate200, borderRadius: BorderRadius.circular(AppRadius.full), ), ), ), const SizedBox(height: AppSpacing.sm), _buildHeader(), _buildTabBar(), Expanded(child: _buildTabContent()), ], ), ), ); } Widget _buildHeader() { return Container( height: 56, padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ SizedBox( width: AppSpacing.xxl * 2, height: AppSpacing.xxl * 2, child: TextButton( onPressed: () => Navigator.pop(context), style: TextButton.styleFrom( padding: const EdgeInsets.all(AppSpacing.none), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.full), ), ), child: const Icon( LucideIcons.x, size: AppSpacing.xxl, color: AppColors.slate700, ), ), ), Text( _isEditing ? '编辑日程' : '新建日程', style: const TextStyle( fontSize: 17, fontWeight: FontWeight.w600, color: AppColors.slate900, ), ), ValueListenableBuilder( valueListenable: _titleController, builder: (context, value, child) { final enabled = value.text.trim().isNotEmpty && !_saving; return SizedBox( height: AppSpacing.xxl * 2, child: TextButton( onPressed: enabled ? _saveEvent : null, style: TextButton.styleFrom( padding: const EdgeInsets.symmetric( horizontal: AppSpacing.md, ), minimumSize: const Size(AppSpacing.none, AppSpacing.none), tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), child: _saving ? const AppLoadingIndicator( variant: AppLoadingVariant.button, size: 18, trackColor: AppColors.blue200, ) : Text( '保存', style: TextStyle( fontSize: 17, fontWeight: FontWeight.w600, color: enabled ? 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( keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildTextField( '标题', _titleController, '请输入日程标题', autofocus: !_isEditing, ), const SizedBox(height: 20), _buildDateTimePicker('开始', _startDate, _startTime, (date, time) { setState(() { _startDate = date; _startTime = time; if (_endDate != null && _endTime != null) { final endDateTime = DateTime( _endDate!.year, _endDate!.month, _endDate!.day, _endTime!.hour, _endTime!.minute, ); final startDateTime = DateTime( date.year, date.month, date.day, time.hour, time.minute, ); if (endDateTime.isBefore(startDateTime) || endDateTime.isAtSameMomentAs(startDateTime)) { _endDate = date; _endTime = time.add(const Duration(hours: 1)); } } }); }), const SizedBox(height: 20), _buildDateTimePicker( '结束', _endDate ?? _startDate, _endTime ?? _startTime, (date, time) { setState(() { final startDateTime = DateTime( _startDate.year, _startDate.month, _startDate.day, _startTime.hour, _startTime.minute, ); final endDateTime = DateTime( date.year, date.month, date.day, time.hour, time.minute, ); if (endDateTime.isBefore(startDateTime) || endDateTime.isAtSameMomentAs(startDateTime)) { _endDate = _startDate; _endTime = _startTime.add(const Duration(hours: 1)); } else { _endDate = date; _endTime = time; } }); }, isOptional: true, minTime: DateTime( _startDate.year, _startDate.month, _startDate.day, _startTime.hour, _startTime.minute, ), ), ], ), ); } Widget _buildAdvancedTab() { return SingleChildScrollView( keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildTextField('描述', _descriptionController, '请输入描述'), const SizedBox(height: 20), _buildTextField('地点', _locationController, '请输入地点'), const SizedBox(height: 20), _buildReminderPicker(), const SizedBox(height: 20), _buildColorPicker(), const SizedBox(height: 20), _buildTextField('备注', _notesController, '请输入备注', maxLines: 3), ], ), ); } Widget _buildTextField( String label, TextEditingController controller, String hint, { int maxLines = 1, bool autofocus = false, }) { return AppSheetInputField( controller: controller, label: label, hint: hint, maxLines: maxLines, autofocus: autofocus, ); } Widget _buildDateTimePicker( String label, DateTime date, DateTime time, Function(DateTime, DateTime) onChanged, { bool isOptional = false, DateTime? minTime, }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label + (isOptional ? '(可选)' : ''), style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.slate700, ), ), const SizedBox(height: 8), InkWell( onTap: () async { final picked = await _pickDateTime(date, time, minTime: minTime); if (picked == null) { return; } onChanged(picked.$1, picked.$2); }, borderRadius: BorderRadius.circular(AppRadius.md), child: Container( padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( color: AppColors.slate50, borderRadius: BorderRadius.circular(AppRadius.md), border: Border.all(color: AppColors.borderSecondary), ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ const Icon( LucideIcons.calendar, size: 16, color: AppColors.slate600, ), const SizedBox(width: AppSpacing.sm), Expanded( child: Text( _formatDateTimeLabel(date, time), style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w500, color: AppColors.slate900, ), ), ), const Icon( LucideIcons.chevronRight, size: 16, color: AppColors.slate400, ), ], ), ), ), ], ); } String _formatDateTimeLabel(DateTime date, DateTime time) { return '${date.year}年${date.month}月${date.day}日 ${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}'; } Future<(DateTime, DateTime)?> _pickDateTime( DateTime date, DateTime time, { DateTime? minTime, }) async { final result = await showModalBottomSheet<(DateTime, DateTime)>( context: context, backgroundColor: Colors.transparent, isScrollControlled: true, builder: (context) => _DateTimePickerSheet( initialDate: date, initialTime: time, minTime: minTime, ), ); return result; } 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.toARGB32().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(), ), ], ); } Widget _buildReminderPicker() { final options = _buildReminderOptions(); String labelOf(int? value) { if (value == null) { return '无提醒'; } if (value == 0) { return '准时提醒'; } return '开始前$value分钟'; } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '提醒时间', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.slate700, ), ), const SizedBox(height: 8), Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(AppRadius.lg), border: Border.all(color: AppColors.border), ), child: DropdownButtonHideUnderline( child: DropdownButton( value: _reminderMinutes, isExpanded: true, items: options .map( (value) => DropdownMenuItem( value: value, child: Text( labelOf(value), style: const TextStyle( fontSize: 14, color: AppColors.slate700, ), ), ), ) .toList(), onChanged: (value) { setState(() { _reminderMinutes = value; }); }, ), ), ), ], ); } int? _sanitizeReminderMinutes(int? minutes) { if (minutes == null || minutes < 0) { return null; } return minutes; } List _buildReminderOptions() { final current = _sanitizeReminderMinutes(_reminderMinutes); final nonNull = _defaultReminderOptions.whereType().toSet(); if (current != null) { nonNull.add(current); } final sorted = nonNull.toList()..sort(); return [null, ...sorted]; } Future _saveEvent() async { if (_titleController.text.trim().isEmpty || _saving) return; setState(() { _saving = true; }); 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, reminderMinutes: _reminderMinutes, attachments: const [], version: widget.editingEvent?.metadata?.version ?? 1, ); final event = ScheduleItemModel( id: _isEditing ? widget.editingEvent!.id : 'evt_${DateTime.now().millisecondsSinceEpoch}', ownerId: widget.editingEvent?.ownerId ?? '', title: _titleController.text.trim(), description: _descriptionController.text.trim().isNotEmpty ? _descriptionController.text.trim() : null, startAt: startAt, endAt: endAt, metadata: metadata, ); try { final service = sl(); late final ScheduleItemModel saved; if (_isEditing) { saved = await service.updateEvent(event); } else { saved = await service.addEvent(event); } try { final notificationService = sl(); await notificationService.upsertEventReminder(saved); } catch (e) { if (mounted) { Toast.show(context, '提醒创建失败:$e', type: ToastType.warning); } } widget.onSaved?.call(); if (mounted) { Navigator.pop(context); } } catch (e) { if (mounted) { Toast.show(context, '保存失败: $e', type: ToastType.error); } } finally { if (mounted) { setState(() { _saving = false; }); } } } } class _DateTimePickerSheet extends StatefulWidget { final DateTime initialDate; final DateTime initialTime; final DateTime? minTime; const _DateTimePickerSheet({ required this.initialDate, required this.initialTime, this.minTime, }); @override State<_DateTimePickerSheet> createState() => _DateTimePickerSheetState(); } class _DateTimePickerSheetState extends State<_DateTimePickerSheet> { late int _selectedYear; late int _selectedMonth; late int _selectedDay; late int _selectedHour; late int _selectedMinute; late FixedExtentScrollController _yearController; late FixedExtentScrollController _monthController; late FixedExtentScrollController _dayController; late FixedExtentScrollController _hourController; late FixedExtentScrollController _minuteController; static final int _baseYear = DateTime.now().year; static final List _years = List.generate(21, (i) => _baseYear - 10 + i); static final List _months = List.generate(12, (i) => i + 1); static final List _allHours = List.generate(24, (i) => i); static final List _allMinutes = List.generate(60, (i) => i); List _days = []; late List _filteredHours; late List _filteredMinutes; List _getFilteredHours() { if (widget.minTime == null) return _allHours; final minDate = widget.minTime!; if (_selectedYear > minDate.year || (_selectedYear == minDate.year && _selectedMonth > minDate.month) || (_selectedYear == minDate.year && _selectedMonth == minDate.month && _selectedDay > minDate.day)) { return _allHours; } if (_selectedYear == minDate.year && _selectedMonth == minDate.month && _selectedDay == minDate.day) { return _allHours.where((h) => h > minDate.hour).toList(); } return _allHours; } List _getFilteredMinutes() { if (widget.minTime == null) return _allMinutes; final minDate = widget.minTime!; if (_selectedYear > minDate.year || (_selectedYear == minDate.year && _selectedMonth > minDate.month) || (_selectedYear == minDate.year && _selectedMonth == minDate.month && _selectedDay > minDate.day)) { return _allMinutes; } if (_selectedYear == minDate.year && _selectedMonth == minDate.month && _selectedDay == minDate.day && _selectedHour == minDate.hour) { return _allMinutes.where((m) => m > minDate.minute).toList(); } return _allMinutes; } @override void initState() { super.initState(); _selectedYear = widget.initialDate.year; _selectedMonth = widget.initialDate.month; _selectedDay = widget.initialDate.day; _selectedHour = widget.initialTime.hour; _selectedMinute = widget.initialTime.minute; _filteredHours = _getFilteredHours(); _filteredMinutes = _getFilteredMinutes(); _updateDays(); _yearController = FixedExtentScrollController( initialItem: _years.indexOf(_selectedYear), ); _monthController = FixedExtentScrollController( initialItem: _selectedMonth - 1, ); _dayController = FixedExtentScrollController(initialItem: _selectedDay - 1); _hourController = FixedExtentScrollController( initialItem: _filteredHours.indexOf(_selectedHour), ); _minuteController = FixedExtentScrollController( initialItem: _filteredMinutes.indexOf(_selectedMinute), ); } void _updateDays() { _days = List.generate( DateTime(_selectedYear, _selectedMonth + 1, 0).day, (i) => i + 1, ); } @override void dispose() { _yearController.dispose(); _monthController.dispose(); _dayController.dispose(); _hourController.dispose(); _minuteController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Container( height: 420, decoration: const BoxDecoration( color: AppColors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), child: Column( children: [ _buildHeader(), Expanded( child: Row( children: [ Expanded( flex: 3, child: Column( children: [ _buildPickerLabel('日期'), Expanded( child: Row( children: [ Expanded( child: _buildPicker(_years, _yearController, (v) { setState(() { _selectedYear = v; _updateDays(); if (_selectedDay > _days.length) { _selectedDay = _days.length; _dayController.jumpToItem(_selectedDay - 1); } }); }, (v) => '$v'), ), const Text( '年', style: TextStyle( fontSize: 14, color: AppColors.slate600, ), ), Expanded( child: _buildPicker(_months, _monthController, ( v, ) { setState(() { _selectedMonth = v; _updateDays(); if (_selectedDay > _days.length) { _selectedDay = _days.length; _dayController.jumpToItem(_selectedDay - 1); } }); }, (v) => '$v'), ), const Text( '月', style: TextStyle( fontSize: 14, color: AppColors.slate600, ), ), Expanded( child: _buildPicker( _days, _dayController, (v) => setState(() => _selectedDay = v), (v) => '$v', ), ), const Text( '日', style: TextStyle( fontSize: 14, color: AppColors.slate600, ), ), ], ), ), ], ), ), Container(width: 1, height: 180, color: AppColors.border), Expanded( flex: 2, child: Column( children: [ _buildPickerLabel('时间'), Expanded( child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Expanded( child: _buildPicker( _filteredHours, _hourController, (v) { setState(() { _selectedHour = v; _filteredMinutes = _getFilteredMinutes(); if (_selectedMinute > _filteredMinutes.last) { _selectedMinute = _filteredMinutes.isNotEmpty ? _filteredMinutes.last : 0; _minuteController.jumpToItem( _filteredMinutes.indexOf( _selectedMinute, ), ); } }); }, (v) => v.toString().padLeft(2, '0'), itemExtent: 50, ), ), const Text( ' : ', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: AppColors.slate600, ), ), Expanded( child: _buildPicker( _filteredMinutes, _minuteController, (v) => setState(() => _selectedMinute = v), (v) => v.toString().padLeft(2, '0'), itemExtent: 50, ), ), ], ), ), ], ), ), ], ), ), ], ), ); } Widget _buildHeader() { return Container( height: 56, padding: const EdgeInsets.symmetric(horizontal: 16), decoration: const BoxDecoration( border: Border(bottom: BorderSide(color: AppColors.border)), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ GestureDetector( onTap: () => Navigator.pop(context), child: const Text( '取消', style: TextStyle(fontSize: 17, color: AppColors.slate600), ), ), const Text( '选择时间', style: TextStyle( fontSize: 17, fontWeight: FontWeight.w600, color: AppColors.slate900, ), ), GestureDetector( onTap: () { Navigator.pop(context, ( DateTime(_selectedYear, _selectedMonth, _selectedDay), DateTime(2000, 1, 1, _selectedHour, _selectedMinute), )); }, child: const Text( '确定', style: TextStyle( fontSize: 17, fontWeight: FontWeight.w600, color: AppColors.blue600, ), ), ), ], ), ); } Widget _buildPickerLabel(String label) { return Padding( padding: const EdgeInsets.symmetric(vertical: 12), child: Text( label, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.slate700, ), ), ); } Widget _buildPicker( List items, FixedExtentScrollController controller, ValueChanged onChanged, String Function(int) formatter, { double itemExtent = 40, }) { return CupertinoPicker( scrollController: controller, itemExtent: itemExtent, magnification: 1.2, squeeze: 0.8, useMagnifier: true, onSelectedItemChanged: (index) => onChanged(items[index]), selectionOverlay: Container( decoration: BoxDecoration( border: Border.symmetric( horizontal: BorderSide( color: AppColors.blue100.withValues(alpha: 0.5), width: 1, ), ), ), ), children: List.generate(items.length, (index) { return Center( child: Text( formatter(items[index]), style: const TextStyle(fontSize: 18, color: AppColors.slate900), ), ); }), ); } }