import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:lucide_icons/lucide_icons.dart'; import '../../../../core/di/injection.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; 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 { 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 _saving = false; List _attachments = const []; 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'; _attachments = List.from( event.metadata?.attachments ?? const [], ); } else { final now = widget.initialDate ?? DateTime.now(); _startDate = now; _startTime = now; _endDate = now; _endTime = now.add(const Duration(hours: 1)); } _titleController.addListener(() => setState(() {})); } @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; 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)) { _endDate = date; _endTime = 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), _buildAttachmentsSection(), const SizedBox(height: 20), _buildTextField('备注', _notesController, '请输入备注', maxLines: 3), ], ), ); } Widget _buildAttachmentsSection() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( '附件', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.slate700, ), ), InkWell( onTap: _showAddAttachmentDialog, borderRadius: BorderRadius.circular(AppRadius.full), child: Container( padding: const EdgeInsets.symmetric( horizontal: AppSpacing.md, vertical: AppSpacing.xs, ), decoration: BoxDecoration( color: AppColors.blue50, borderRadius: BorderRadius.circular(AppRadius.full), border: Border.all(color: AppColors.borderQuaternary), ), child: const Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon(LucideIcons.plus, size: 14, color: AppColors.blue600), SizedBox(width: AppSpacing.xs), Text( '添加附件', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: AppColors.blue600, ), ), ], ), ), ), ], ), const SizedBox(height: AppSpacing.sm), if (_attachments.isEmpty) Container( width: double.infinity, padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( color: AppColors.slate50, borderRadius: BorderRadius.circular(AppRadius.md), border: Border.all(color: AppColors.borderSecondary), ), child: const Text( '暂无附件,点击右上角添加', style: TextStyle(color: AppColors.slate500, fontSize: 13), ), ), ..._attachments.asMap().entries.map((entry) { final index = entry.key; final item = entry.value; return Container( margin: const EdgeInsets.only(top: AppSpacing.sm), padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( color: AppColors.white, borderRadius: BorderRadius.circular(AppRadius.md), border: Border.all(color: AppColors.messageCardBorder), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: Text( item.name, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: AppColors.slate800, ), ), ), Container( padding: const EdgeInsets.symmetric( horizontal: AppSpacing.sm, vertical: AppSpacing.xs, ), decoration: BoxDecoration( color: AppColors.surfaceInfo, borderRadius: BorderRadius.circular(AppRadius.full), ), child: Text( item.type, style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: AppColors.blue600, ), ), ), const SizedBox(width: AppSpacing.sm), GestureDetector( onTap: () { setState(() { final next = List.from(_attachments); next.removeAt(index); _attachments = next; }); }, child: const Icon( LucideIcons.trash, size: 16, color: AppColors.red500, ), ), ], ), if ((item.url ?? '').isNotEmpty) ...[ const SizedBox(height: AppSpacing.xs), Text( '链接: ${item.url}', style: const TextStyle( fontSize: 12, color: AppColors.slate500, ), ), ], if ((item.note ?? '').isNotEmpty) ...[ const SizedBox(height: AppSpacing.xs), Text( '备注: ${item.note}', style: const TextStyle( fontSize: 12, color: AppColors.slate500, ), ), ], ], ), ); }), ], ); } Future _showAddAttachmentDialog() async { final nameController = TextEditingController(); final urlController = TextEditingController(); final noteController = TextEditingController(); final contentController = TextEditingController(); var type = 'document'; try { final created = await showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (sheetContext) => StatefulBuilder( builder: (sheetContext, setSheetState) { return Container( padding: EdgeInsets.only( left: AppSpacing.lg, right: AppSpacing.lg, top: AppSpacing.lg, bottom: MediaQuery.of(sheetContext).viewInsets.bottom + AppSpacing.lg, ), decoration: const BoxDecoration( color: AppColors.white, borderRadius: BorderRadius.vertical( top: Radius.circular(AppRadius.xxl), ), ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '添加附件', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w700, color: AppColors.slate900, ), ), const SizedBox(height: AppSpacing.md), _buildTextField('名称', nameController, '例如:会议纪要.pdf'), const SizedBox(height: AppSpacing.md), _buildTextField('链接', urlController, 'https://...'), const SizedBox(height: AppSpacing.md), _buildTextField('备注', noteController, '备注信息'), const SizedBox(height: AppSpacing.md), _buildTextField('内容', contentController, '提醒内容', maxLines: 2), const SizedBox(height: AppSpacing.md), Wrap( spacing: AppSpacing.sm, children: ['document', 'reminder'].map((item) { final selected = item == type; return ChoiceChip( label: Text(item), selected: selected, onSelected: (_) { setSheetState(() { type = item; }); }, ); }).toList(), ), const SizedBox(height: AppSpacing.lg), Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: OutlinedButton( onPressed: () => Navigator.pop(sheetContext), child: const Text('取消'), ), ), const SizedBox(width: AppSpacing.sm), Expanded( child: ElevatedButton( onPressed: () { final name = nameController.text.trim(); if (name.isEmpty) { return; } Navigator.pop( sheetContext, Attachment( name: name, url: urlController.text.trim().isEmpty ? null : urlController.text.trim(), note: noteController.text.trim().isEmpty ? null : noteController.text.trim(), content: contentController.text.trim().isEmpty ? null : contentController.text.trim(), type: type, ), ); }, child: const Text('确认添加'), ), ), ], ), ], ), ); }, ), ); if (created != null && mounted) { setState(() { _attachments = [..._attachments, created]; }); } } finally { nameController.dispose(); urlController.dispose(); noteController.dispose(); contentController.dispose(); } } 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: const Color(0xFFF1F5F9), 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), InkWell( onTap: () async { final picked = await _pickDateTime(date, time); 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, ) async { final result = await showModalBottomSheet<(DateTime, DateTime)>( context: context, backgroundColor: Colors.transparent, isScrollControlled: true, builder: (context) => _DateTimePickerSheet(initialDate: date, initialTime: time), ); 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(), ), ], ); } 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, attachments: _attachments, version: widget.editingEvent?.metadata?.version ?? 1, ); 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, ); try { final service = sl(); debugPrint('CalendarService: $service'); debugPrint('Is mock: ${service.runtimeType}'); if (_isEditing) { await service.updateEvent(event); } else { await service.addEvent(event); } widget.onSaved?.call(); if (mounted) { Navigator.pop(context); } } catch (e, stack) { debugPrint('Save error: $e'); debugPrint('Stack: $stack'); if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('保存失败: $e'))); } } finally { if (mounted) { setState(() { _saving = false; }); } } } } class _DateTimePickerSheet extends StatefulWidget { final DateTime initialDate; final DateTime initialTime; const _DateTimePickerSheet({ required this.initialDate, required this.initialTime, }); @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 _hours = List.generate(24, (i) => i); static final List _minutes = List.generate(60, (i) => i); List _days = []; @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; _updateDays(); _yearController = FixedExtentScrollController( initialItem: _years.indexOf(_selectedYear), ); _monthController = FixedExtentScrollController( initialItem: _selectedMonth - 1, ); _dayController = FixedExtentScrollController(initialItem: _selectedDay - 1); _hourController = FixedExtentScrollController(initialItem: _selectedHour); _minuteController = FixedExtentScrollController( initialItem: _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( _hours, _hourController, (v) => setState(() => _selectedHour = v), (v) => v.toString().padLeft(2, '0'), itemExtent: 50, ), ), const Text( ' : ', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: AppColors.slate600, ), ), Expanded( child: _buildPicker( _minutes, _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), ), ); }), ); } }