import 'package:flutter/material.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 '../../../home/ui/navigation/home_return_policy.dart'; import '../../../../shared/widgets/app_pull_refresh_feedback.dart'; import '../../../../shared/widgets/app_pressable.dart'; import '../../../../shared/widgets/back_title_page_header.dart'; import '../../../../shared/widgets/error_retry_surface.dart'; import '../../../../shared/widgets/full_screen_loading.dart'; import '../../../../shared/widgets/toast/toast.dart'; import '../../../../shared/widgets/toast/toast_type.dart'; import '../../../calendar/ui/calendar_state_manager.dart'; import '../../../calendar/ui/widgets/bottom_dock.dart'; import '../../data/todo_api.dart'; import '../widgets/todo_drag_item.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; bool _isPullRefreshing = false; bool _loadingTodosRequest = false; String? _error; String? _draggingTodoId; int? _dragTargetQuadrant; int? _dragInsertIndex; bool get _isDragging => _draggingTodoId != null; void _onDragStart(String todoId) { setState(() { _draggingTodoId = todoId; _dragTargetQuadrant = null; _dragInsertIndex = null; }); } void _onDragEnd() { setState(() { _draggingTodoId = null; _dragTargetQuadrant = null; _dragInsertIndex = null; }); } void _onDragEnterQuadrant(int quadrant) { setState(() { _dragTargetQuadrant = quadrant; }); } void _onDragUpdateInsertIndex(int index) { setState(() { _dragInsertIndex = index; }); } Future _onDrop( String todoId, int targetQuadrant, int insertIndex, ) async { final previousTodos = List.from(_todos); try { final todo = _todos.firstWhere((t) => t.id == todoId); final sourceQuadrant = todo.priority; if (sourceQuadrant == targetQuadrant) { _onDragEnd(); return; } setState(() { final index = _todos.indexWhere((t) => t.id == todoId); if (index != -1) { _todos[index] = _todos[index].copyWith(priority: targetQuadrant); } }); await _todoApi.updateTodoPriority(todoId, targetQuadrant); } catch (e) { if (!mounted) return; setState(() { _todos = previousTodos; }); Toast.show(context, '移动失败', type: ToastType.error); } finally { if (mounted) _onDragEnd(); } } void _onDragLeave() { // 清除高亮 } @override void initState() { super.initState(); _loadTodos(); } Future _loadTodos({bool showPageLoader = true}) async { if (_loadingTodosRequest || _isPullRefreshing) { return; } _loadingTodosRequest = true; setState(() { if (showPageLoader) { _isLoading = true; _error = null; } else { _isPullRefreshing = true; } }); try { final todos = await _todoApi.getTodos(status: 'pending'); if (!mounted) { return; } setState(() { _todos = todos; _isLoading = false; _isPullRefreshing = false; _error = null; }); } catch (e) { if (!mounted) { return; } if (showPageLoader) { setState(() { _error = e.toString(); _isLoading = false; _isPullRefreshing = false; }); } else { setState(() => _isPullRefreshing = false); Toast.show(context, '刷新失败,请稍后重试', type: ToastType.error); } } finally { _loadingTodosRequest = false; } } Future _onPullRefresh() async { await _loadTodos(showPageLoader: 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(AppRoutes.todoDetail(todo.id)); } Future _addTodo() async { final created = await context.push(AppRoutes.todoCreate); if (created == true) { await _loadTodos(); } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.todoBg, body: PopScope( canPop: false, onPopInvokedWithResult: (didPop, result) { if (!didPop) { returnToHomePreserveState(context); } }, child: SafeArea( child: Column( children: [ _buildHeader(), Expanded(child: _buildContent(withScroll: true)), _buildBottomDock(), ], ), ), ), ); } Widget _buildHeader() { return BackTitlePageHeader( title: '待办事项', showBackButton: false, trailing: Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ AppPressable( borderRadius: BorderRadius.circular(AppRadius.full), onTap: _loadTodos, child: Container( width: 36, height: 36, decoration: BoxDecoration( color: AppColors.messageBtnWrap, borderRadius: BorderRadius.circular(AppRadius.full), border: Border.all(color: AppColors.messageBtnBorder), ), child: const Icon( LucideIcons.refreshCcw, size: 18, color: AppColors.slate600, ), ), ), const SizedBox(width: AppSpacing.sm), AppPressable( borderRadius: BorderRadius.circular(AppRadius.full), onTap: _addTodo, child: Container( width: 36, height: 36, decoration: BoxDecoration( color: AppColors.blue600, borderRadius: BorderRadius.circular(AppRadius.full), boxShadow: [ BoxShadow( color: AppColors.blue300.withValues(alpha: 0.28), blurRadius: AppRadius.lg, offset: const Offset(0, AppSpacing.xs), ), ], ), child: const Icon( LucideIcons.plus, size: 18, color: AppColors.white, ), ), ), ], ), ); } Widget _buildContent({bool withScroll = false}) { if (_isLoading) { return const FullScreenLoading(); } if (_error != null) { return ErrorRetrySurface(message: '加载失败: $_error', onRetry: _loadTodos); } Widget content = Column( mainAxisSize: MainAxisSize.min, children: [ _buildQuadrant( title: '重要紧急', textColor: AppColors.g1Text, dividerColor: AppColors.g1Divider, borderColor: AppColors.g1Border, items: _importantUrgent, quadrantValue: 1, onComplete: _completeTodo, onTap: _navigateToDetail, ), const SizedBox(height: 12), _buildQuadrant( title: '紧急不重要', textColor: AppColors.g2Text, dividerColor: AppColors.g2Divider, borderColor: AppColors.g2Border, items: _urgentNotImportant, quadrantValue: 3, onComplete: _completeTodo, onTap: _navigateToDetail, ), const SizedBox(height: 12), _buildQuadrant( title: '重要不紧急', textColor: AppColors.g3Text, dividerColor: AppColors.g3Divider, borderColor: AppColors.g3Border, items: _importantNotUrgent, quadrantValue: 2, onComplete: _completeTodo, onTap: _navigateToDetail, ), ], ); if (withScroll) { return Stack( children: [ RefreshIndicator.noSpinner( onRefresh: _onPullRefresh, child: SingleChildScrollView( padding: const EdgeInsets.only( left: 16, right: 16, top: 4, bottom: 96, ), child: content, ), ), Align( alignment: Alignment.topCenter, child: AppPullRefreshFeedback(visible: _isPullRefreshing), ), ], ); } return Padding( padding: const EdgeInsets.fromLTRB(16, 4, 16, 16), child: content, ); } Widget _buildQuadrant({ required String title, required Color textColor, required Color dividerColor, required Color borderColor, required List items, required int quadrantValue, required Future Function(TodoResponse) onComplete, required void Function(TodoResponse) onTap, }) { return Container( decoration: BoxDecoration( color: AppColors.todoCardBg, borderRadius: BorderRadius.circular(14), border: Border.all(color: borderColor, width: 1), ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildQuadrantHeader(title, textColor, items.length), Container(height: 1, color: dividerColor), const SizedBox(height: 8), Padding( padding: const EdgeInsets.fromLTRB(6, 0, 6, 8), child: DragTarget( onWillAcceptWithDetails: (details) { _onDragEnterQuadrant(quadrantValue); return true; }, onAcceptWithDetails: (details) { final parts = details.data.split(':'); final todoId = parts[0]; _onDrop(todoId, quadrantValue, 0); }, onLeave: (_) { _onDragLeave(); }, builder: (context, candidateData, rejectedData) { final isDragOver = candidateData.isNotEmpty; return Container( decoration: BoxDecoration( color: isDragOver ? AppColors.blue50.withValues(alpha: 0.3) : Colors.transparent, borderRadius: BorderRadius.circular(8), border: isDragOver ? Border.all(color: AppColors.blue400, width: 2) : null, ), child: items.isEmpty ? SizedBox( height: 60, child: Center( child: Text( '暂无待办', style: TextStyle( fontFamily: 'Inter', fontSize: 13, color: AppColors.slate400, ), ), ), ) : _buildQuadrantItemList( items, quadrantValue, onComplete, onTap, ), ); }, ), ), ], ), ); } Widget _buildQuadrantHeader(String title, Color textColor, int itemCount) { return Padding( padding: const EdgeInsets.fromLTRB(10, 10, 10, 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( title, style: TextStyle( fontFamily: 'Inter', fontSize: 15, fontWeight: FontWeight.w700, color: textColor, ), ), Text( '${itemCount}项', style: TextStyle( fontFamily: 'Inter', fontSize: 12, fontWeight: FontWeight.w700, color: textColor, ), ), ], ), ); } Widget _buildQuadrantItemList( List items, int quadrantValue, Future Function(TodoResponse) onComplete, void Function(TodoResponse) onTap, ) { return Column( mainAxisSize: MainAxisSize.min, children: items.map((item) { return TodoDragItem( todo: item, quadrant: quadrantValue, onDragStarted: () => _onDragStart(item.id), onDragEnd: _onDragEnd, child: _TodoItemWidget( item: item, onComplete: () => onComplete(item), onTap: () => onTap(item), ), ); }).toList(), ); } 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(AppRoutes.calendarMonth); } else { context.push('${AppRoutes.calendarDayWeek}?date=$dateStr'); } }, onHomeTap: () => returnToHomePreserveState(context), ); } } 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, ); }, ), ), ], ), ), ); } }