import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import '../../../../core/di/injection.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_button.dart'; import '../../../../shared/widgets/toast/toast.dart'; import '../../../../shared/widgets/toast/toast_type.dart'; import '../../../calendar/data/calendar_api.dart'; import '../../../calendar/ui/calendar_state_manager.dart'; import '../../../calendar/ui/widgets/bottom_dock.dart'; import '../../data/todo_api.dart'; class TodoQuadrantsScreen extends StatefulWidget { const TodoQuadrantsScreen({super.key}); @override State createState() => _TodoQuadrantsScreenState(); } class _TodoQuadrantsScreenState extends State { final TodoApi _todoApi = sl(); List _todos = []; bool _isLoading = true; String? _error; @override void initState() { super.initState(); _loadTodos(); } Future _loadTodos() async { setState(() { _isLoading = true; _error = null; }); try { final todos = await _todoApi.getTodos(status: 'pending'); setState(() { _todos = todos; _isLoading = false; }); } catch (e) { setState(() { _error = e.toString(); _isLoading = false; }); } } List get _importantUrgent => _todos.where((t) => t.priority == 1).toList(); List get _urgentNotImportant => _todos.where((t) => t.priority == 3).toList(); List get _importantNotUrgent => _todos.where((t) => t.priority == 2).toList(); Future _completeTodo(TodoResponse todo) async { try { await _todoApi.completeTodo(todo.id); if (mounted) { Toast.show(context, '已完成', type: ToastType.success); } try { await _loadTodos(); } catch (_) { // ignore reload error } } catch (e) { if (mounted) { Toast.show(context, '完成失败: $e', type: ToastType.error); } } } void _navigateToDetail(TodoResponse todo) { context.push('/todo/${todo.id}'); } Future _addTodo() async { final result = await showModalBottomSheet>( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) => const _AddTodoSheet(), ); if (result != null) { try { await _todoApi.createTodo( title: result['title'] as String, description: result['description'] as String?, priority: result['priority'] as int, scheduleItemIds: (result['schedule_item_ids'] as List?) ?? [], ); await _loadTodos(); } catch (e) { if (mounted) { Toast.show(context, '创建失败: $e', type: ToastType.error); } } } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.todoBg, floatingActionButton: FloatingActionButton( onPressed: _addTodo, backgroundColor: AppColors.blue600, child: const Icon(Icons.add, color: Colors.white), ), body: PopScope( canPop: false, onPopInvokedWithResult: (didPop, result) { if (!didPop) { context.go('/home'); } }, child: SafeArea( child: Column( children: [ _buildHeader(), Expanded(child: _buildContent()), _buildBottomDock(), ], ), ), ), ); } Widget _buildHeader() { return SizedBox( height: 72, child: Padding( padding: const EdgeInsets.only(left: 16, right: 16, top: 14, bottom: 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( '待办事项', style: TextStyle( fontFamily: 'Inter', fontSize: 22, fontWeight: FontWeight.w700, color: AppColors.slate900, ), ), IconButton( onPressed: _loadTodos, icon: const Icon(Icons.refresh, color: AppColors.slate600), ), ], ), ), ); } Widget _buildContent() { if (_isLoading) { return const Center(child: CircularProgressIndicator()); } if (_error != null) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('加载失败: $_error', style: const TextStyle(color: Colors.red)), const SizedBox(height: 16), AppButton(text: '重试', onPressed: _loadTodos), ], ), ); } return RefreshIndicator( onRefresh: _loadTodos, child: Padding( padding: const EdgeInsets.only(left: 16, right: 16, top: 4, bottom: 96), child: ListView( children: [ _buildQuadrant( title: '重要紧急', textColor: AppColors.g1Text, dividerColor: AppColors.g1Divider, borderColor: AppColors.g1Border, items: _importantUrgent, onComplete: _completeTodo, onTap: _navigateToDetail, ), const SizedBox(height: 12), _buildQuadrant( title: '紧急不重要', textColor: AppColors.g2Text, dividerColor: AppColors.g2Divider, borderColor: AppColors.g2Border, items: _urgentNotImportant, onComplete: _completeTodo, onTap: _navigateToDetail, ), const SizedBox(height: 12), _buildQuadrant( title: '重要不紧急', textColor: AppColors.g3Text, dividerColor: AppColors.g3Divider, borderColor: AppColors.g3Border, items: _importantNotUrgent, onComplete: _completeTodo, onTap: _navigateToDetail, ), ], ), ), ); } Widget _buildQuadrant({ required String title, required Color textColor, required Color dividerColor, required Color borderColor, required List items, required Future Function(TodoResponse) onComplete, required void Function(TodoResponse) onTap, }) { return Container( width: double.infinity, padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: AppColors.todoCardBg, borderRadius: BorderRadius.circular(14), border: Border.all(color: borderColor, width: 1), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( title, style: TextStyle( fontFamily: 'Inter', fontSize: 15, fontWeight: FontWeight.w700, color: textColor, ), ), Text( '${items.length}项', style: TextStyle( fontFamily: 'Inter', fontSize: 12, fontWeight: FontWeight.w700, color: textColor, ), ), ], ), const SizedBox(height: 8), Container(height: 1, color: dividerColor), const SizedBox(height: 8), if (items.isEmpty) const Padding( padding: EdgeInsets.symmetric(vertical: 16), child: Center( child: Text( '暂无待办', style: TextStyle( fontFamily: 'Inter', fontSize: 13, color: AppColors.slate400, ), ), ), ) else ...items.map( (item) => _TodoItemWidget( item: item, onComplete: () => onComplete(item), onTap: () => onTap(item), ), ), ], ), ); } Widget _buildBottomDock() { return BottomDock( activeTab: DockTab.todo, onTodoTap: () {}, onCalendarTap: () { final manager = sl(); final viewType = manager.viewType; final date = manager.selectedDate; final dateStr = '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; if (viewType == CalendarViewType.month) { context.push('/calendar/month'); } else { context.push('/calendar/dayweek?date=$dateStr'); } }, onHomeTap: () => context.go('/home'), ); } } class _TodoItemWidget extends StatefulWidget { final TodoResponse item; final VoidCallback onComplete; final VoidCallback onTap; const _TodoItemWidget({ required this.item, required this.onComplete, required this.onTap, }); @override State<_TodoItemWidget> createState() => _TodoItemWidgetState(); } class _TodoItemWidgetState extends State<_TodoItemWidget> with SingleTickerProviderStateMixin { bool _isChecked = false; late AnimationController _controller; late Animation _scaleAnimation; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(milliseconds: 200), vsync: this, ); _scaleAnimation = Tween( begin: 0.0, end: 1.0, ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutBack)); } @override void dispose() { _controller.dispose(); super.dispose(); } void _handleCheckTap() async { if (_isChecked) return; setState(() { _isChecked = true; }); _controller.forward().then((_) { widget.onComplete(); }); } @override Widget build(BuildContext context) { return GestureDetector( onTap: widget.onTap, child: SizedBox( height: 42, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: Text( widget.item.title, style: const TextStyle( fontFamily: 'Inter', fontSize: 13, fontWeight: FontWeight.w600, color: AppColors.slate700, ), ), ), GestureDetector( onTap: _handleCheckTap, child: AnimatedBuilder( animation: _controller, builder: (context, child) { return Container( width: 20, height: 20, decoration: BoxDecoration( color: _isChecked ? AppColors.blue600 : Colors.white, border: Border.all( color: _isChecked ? AppColors.blue600 : AppColors.slate300, width: 1.5, ), borderRadius: BorderRadius.circular(4), ), child: _isChecked ? Transform.scale( scale: _scaleAnimation.value, child: const Icon( Icons.check, size: 14, color: Colors.white, ), ) : null, ); }, ), ), ], ), ), ); } } class _AddTodoSheet extends StatefulWidget { const _AddTodoSheet(); @override State<_AddTodoSheet> createState() => _AddTodoSheetState(); } class _AddTodoSheetState extends State<_AddTodoSheet> { final _titleController = TextEditingController(); final _descriptionController = TextEditingController(); int _priority = 1; final Set _selectedScheduleItems = {}; @override void dispose() { _titleController.dispose(); _descriptionController.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: [ SingleChildScrollView( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '添加待办', style: TextStyle( fontFamily: 'Inter', fontSize: 20, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 20), TextField( controller: _titleController, decoration: const InputDecoration( labelText: '标题', border: OutlineInputBorder(), ), autofocus: true, ), const SizedBox(height: 16), TextField( controller: _descriptionController, decoration: const InputDecoration( labelText: '描述(可选)', border: OutlineInputBorder(), ), maxLines: 2, ), const SizedBox(height: 16), const Text( '优先级', style: TextStyle( fontFamily: 'Inter', fontSize: 14, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 8), Row( children: [ _PriorityChip( label: '重要紧急', selected: _priority == 1, color: AppColors.g1Border, onTap: () => setState(() => _priority = 1), ), const SizedBox(width: 8), _PriorityChip( label: '紧急不重要', selected: _priority == 3, color: AppColors.g2Border, onTap: () => setState(() => _priority = 3), ), const SizedBox(width: 8), _PriorityChip( label: '重要不紧急', selected: _priority == 2, color: AppColors.g3Border, onTap: () => setState(() => _priority = 2), ), ], ), ], ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: Row( children: [ Text( '关联日历事件', style: TextStyle( fontFamily: 'Inter', fontSize: 14, fontWeight: FontWeight.w600, ), ), ], ), ), const SizedBox(height: 8), Expanded( child: FutureBuilder( future: _loadScheduleItems(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } if (snapshot.hasError) { return Center(child: Text('加载失败: ${snapshot.error}')); } final items = snapshot.data ?? []; if (items.isEmpty) { return const Center(child: Text('暂无日历事件')); } return ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 16), itemCount: items.length, itemBuilder: (context, index) { final item = items[index]; final isSelected = _selectedScheduleItems.contains(item.id); return CheckboxListTile( title: Text(item.title), subtitle: Text(_formatDate(item.startAt)), value: isSelected, onChanged: (value) { setState(() { if (value == true) { _selectedScheduleItems.add(item.id); } else { _selectedScheduleItems.remove(item.id); } }); }, ); }, ); }, ), ), Padding( padding: const EdgeInsets.all(16), child: SizedBox( width: double.infinity, child: AppButton( text: '添加', onPressed: () { if (_titleController.text.trim().isEmpty) { Toast.show(context, '请输入标题', type: ToastType.warning); return; } Navigator.of(context).pop({ 'title': _titleController.text.trim(), 'description': _descriptionController.text.trim().isEmpty ? null : _descriptionController.text.trim(), 'priority': _priority, 'schedule_item_ids': _selectedScheduleItems.toList(), }); }, ), ), ), ], ), ); } Future> _loadScheduleItems() async { final calendarApi = sl(); final now = DateTime.now(); final start = now.subtract(const Duration(days: 30)); final end = now.add(const Duration(days: 90)); final items = await calendarApi.listByRange(startAt: start, endAt: end); return items .map( (e) => _ScheduleItemSimple(id: e.id, title: e.title, startAt: e.startAt), ) .toList(); } String _formatDate(DateTime dt) { return '${dt.year}年${dt.month}月${dt.day}日 ${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; } } class _ScheduleItemSimple { final String id; final String title; final DateTime startAt; _ScheduleItemSimple({ required this.id, required this.title, required this.startAt, }); } class _PriorityChip extends StatelessWidget { final String label; final bool selected; final Color color; final VoidCallback onTap; const _PriorityChip({ required this.label, required this.selected, required this.color, required this.onTap, }); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: selected ? color.withValues(alpha: 0.2) : Colors.transparent, border: Border.all( color: selected ? color : AppColors.slate300, width: selected ? 2 : 1, ), borderRadius: BorderRadius.circular(20), ), child: Text( label, style: TextStyle( fontFamily: 'Inter', fontSize: 12, fontWeight: selected ? FontWeight.w600 : FontWeight.normal, color: selected ? color : AppColors.slate600, ), ), ), ); } }