diff --git a/lib/layout/main_screen.dart b/lib/layout/main_screen.dart index 323164b..b7180c4 100644 --- a/lib/layout/main_screen.dart +++ b/lib/layout/main_screen.dart @@ -7,29 +7,29 @@ import 'package:flisp_app/provider/app_provider.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -class MainScreen extends StatelessWidget { +class MainScreen extends StatefulWidget { const MainScreen({super.key}); + @override + State createState() => _MainScreenState(); +} + +class _MainScreenState extends State { + final GlobalKey _todoPageKey = GlobalKey(); + @override Widget build(BuildContext context) { return Consumer( builder: (context, appState, child) { return Scaffold( - // 侧边抽屉菜单 drawer: const AppDrawer(), - - // 顶部导航栏 appBar: AppBar( - title: const Text('闪灵'), + title: Text(_getAppBarTitle(appState.currentIndex)), backgroundColor: Colors.blue, foregroundColor: Colors.white, elevation: 0, ), - - // 主体内容 body: _buildPage(appState.currentIndex), - - // 底部导航栏 bottomNavigationBar: BottomNavigationBar( currentIndex: appState.currentIndex, onTap: (index) => appState.changeTab(index), @@ -53,12 +53,50 @@ class MainScreen extends StatelessWidget { ), ], ), + floatingActionButton: _buildFloatingActionButton(appState.currentIndex), ); }, ); } - // 根据索引构建对应页面 + Widget? _buildFloatingActionButton(int currentIndex) { + if (currentIndex == 2) { + return FloatingActionButton( + onPressed: _showAddTodoDialog, + backgroundColor: Colors.transparent, + elevation: 0, + child: Container( + width: 50, + height: 50, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + colors: [Colors.orange.shade300, Colors.orange.shade500], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + boxShadow: [ + BoxShadow( + color: Colors.orange.shade100, + blurRadius: 12, + offset: Offset(0, 6), + ), + ], + ), + child: Icon(Icons.add, color: Colors.white, size: 36), + ), + ); + } + return null; + } + + void _showAddTodoDialog() { + // 通过 GlobalKey 调用 TodoPage 的方法 + if (_todoPageKey.currentState != null) { + _todoPageKey.currentState!.showAddTodoDialog(); + } + } + Widget _buildPage(int index) { switch (index) { case 0: @@ -66,11 +104,21 @@ class MainScreen extends StatelessWidget { case 1: return const NotesPage(); case 2: - return const TodoPage(); + return TodoPage(key: _todoPageKey); // 传递 key case 3: return const RemindersPage(); default: return const FlashPage(); } } + + String _getAppBarTitle(int index) { + final titles = { + 0: '闪灵', + 1: '笔记', + 2: '待办事项', + 3: '提醒任务', + }; + return titles[index] ?? '闪灵'; + } } \ No newline at end of file diff --git a/lib/models/todo.dart b/lib/models/todo.dart new file mode 100644 index 0000000..ad476ea --- /dev/null +++ b/lib/models/todo.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; + +class TodoItem { + String id; + String title; + String? description; + bool isCompleted; + DateTime createdAt; + DateTime? dueDate; + TodoPriority priority; + String? category; + + TodoItem({ + required this.id, + required this.title, + this.description, + this.isCompleted = false, + DateTime? createdAt, + this.dueDate, + this.priority = TodoPriority.medium, + this.category, + }) : createdAt = createdAt ?? DateTime.now(); + + TodoItem copyWith({ + String? id, + String? title, + String? description, + bool? isCompleted, + DateTime? createdAt, + DateTime? dueDate, + TodoPriority? priority, + String? category, + }) { + return TodoItem( + id: id ?? this.id, + title: title ?? this.title, + description: description ?? this.description, + isCompleted: isCompleted ?? this.isCompleted, + createdAt: createdAt ?? this.createdAt, + dueDate: dueDate ?? this.dueDate, + priority: priority ?? this.priority, + category: category ?? this.category, + ); + } +} + +enum TodoPriority { + low('低', Color(0xFF757575), Icons.low_priority), + medium('中', Color(0xFFF57C00), Icons.flag), + high('高', Color(0xFFD32F2F), Icons.warning); + + final String label; + final Color color; + final IconData icon; + + const TodoPriority(this.label, this.color, this.icon); +} + +enum TodoTab { + all('全部'), + active('待完成'), + completed('已完成'), + today('今天'); + + final String label; + + const TodoTab(this.label); +} diff --git a/lib/pages/todo_page.dart b/lib/pages/todo_page.dart index e6cce8a..2436874 100644 --- a/lib/pages/todo_page.dart +++ b/lib/pages/todo_page.dart @@ -1,348 +1,79 @@ import 'package:awesome_dialog/awesome_dialog.dart'; -import 'package:flisp_app/models/common.dart'; +import 'package:flisp_app/models/todo.dart'; +import 'package:flisp_app/utils/todo_utils.dart'; +import 'package:flisp_app/widgets/common.dart'; +import 'package:flisp_app/widgets/todo_dialog.dart'; +import 'package:flisp_app/widgets/todo_widget.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; // 用于复制功能 class TodoPage extends StatefulWidget { - const TodoPage({super.key}); + final VoidCallback? onAddTodoPressed; + + const TodoPage({super.key, this.onAddTodoPressed}); @override - State createState() => _TodoPageState(); + State createState() => TodoPageState(); } -// 待办事项数据模型 -class TodoItem { - String id; - String title; - String? description; - bool isCompleted; - DateTime createdAt; - DateTime? dueDate; - Priority priority; - String? category; - - TodoItem({ - required this.id, - required this.title, - this.description, - this.isCompleted = false, - DateTime? createdAt, - this.dueDate, - this.priority = Priority.medium, - this.category, - }) : createdAt = createdAt ?? DateTime.now(); - - TodoItem copyWith({ - String? id, - String? title, - String? description, - bool? isCompleted, - DateTime? createdAt, - DateTime? dueDate, - Priority? priority, - String? category, - }) { - return TodoItem( - id: id ?? this.id, - title: title ?? this.title, - description: description ?? this.description, - isCompleted: isCompleted ?? this.isCompleted, - createdAt: createdAt ?? this.createdAt, - dueDate: dueDate ?? this.dueDate, - priority: priority ?? this.priority, - category: category ?? this.category, - ); +class TodoPageState extends State { + // 添加一个公共方法供外部调用 + void showAddTodoDialog() { + _showAddTodoDialog(); } -} -enum Priority { - low('低', Colors.grey, Icons.low_priority), - medium('中', Colors.orange, Icons.flag), - high('高', Colors.red, Icons.warning); - - final String label; - final Color color; - final IconData icon; - - const Priority(this.label, this.color, this.icon); -} - -enum TodoFilter { - all('全部'), - active('待完成'), - completed('已完成'), - today('今天'); - - final String label; - - const TodoFilter(this.label); -} - -class _TodoPageState extends State { // 待办事项列表 - List _todos = []; - TodoFilter _currentFilter = TodoFilter.all; + final List _todos = [ + // ... 原有的待办事项数据保持不变 + ]; + + TodoTab _currentTab = TodoTab.all; final TextEditingController _titleController = TextEditingController(); final TextEditingController _descriptionController = TextEditingController(); DateTime? _selectedDueDate; - Priority _selectedPriority = Priority.medium; + TodoPriority _selectedPriority = TodoPriority.medium; String? _selectedCategory; // 获取过滤后的待办事项 - List get _filteredTodos { - switch (_currentFilter) { - case TodoFilter.active: - return _todos.where((todo) => !todo.isCompleted).toList(); - case TodoFilter.completed: - return _todos.where((todo) => todo.isCompleted).toList(); - case TodoFilter.today: - final today = DateTime.now(); - return _todos.where((todo) => - todo.dueDate != null && - todo.dueDate!.year == today.year && - todo.dueDate!.month == today.month && - todo.dueDate!.day == today.day - ).toList(); - default: - return _todos; - } + List get _activeTodos { + return getActiveTodos(_currentTab, _todos); } - // 统计数据 - int get _totalCount => _todos.length; - - int get _activeCount => - _todos - .where((todo) => !todo.isCompleted) - .length; - - int get _completedCount => - _todos - .where((todo) => todo.isCompleted) - .length; - @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('待办事项'), - backgroundColor: Colors.orange, - foregroundColor: Colors.white - ), - body: Column( + return buildBody( + child: Column( children: [ // 统计信息卡片 - _buildStatsCard(), - - // 过滤器选项卡 - _buildFilterTabs(), - + buildStatsCard(_todos), + // 选项卡 + buildTabs( + currentTab: _currentTab, + onTabChanged: (value) { + setState(() { + _currentTab = value; + }); + }, + ), // 待办事项列表 - Expanded( - child: _filteredTodos.isEmpty - ? _buildEmptyState() - : _buildTodoList(), - ), - ], - ), - floatingActionButton: FloatingActionButton( - onPressed: _showAddTodoDialog, - backgroundColor: Colors.orange, - child: const Icon(Icons.add), - ), - ); - } - - // 构建统计信息卡片 - Widget _buildStatsCard() { - return Card( - margin: const EdgeInsets.all(12), - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - _buildStatItem('总计', _totalCount, Colors.blue), - _buildStatItem('待完成', _activeCount, Colors.orange), - _buildStatItem('已完成', _completedCount, Colors.green), - ], - ), - ), - ); - } - - Widget _buildStatItem(String label, int count, Color color) { - return Column( - children: [ - Text( - count.toString(), - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: color, - ), - ), - const SizedBox(height: 4), - Text( - label, - style: const TextStyle(fontSize: 12, color: Colors.grey), - ), - ], - ); - } - - // 构建过滤器选项卡 - Widget _buildFilterTabs() { - return SizedBox( - height: 50, - child: ListView( - scrollDirection: Axis.horizontal, - children: TodoFilter.values.map((filter) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: FilterChip( - label: Text(filter.label), - selected: _currentFilter == filter, - onSelected: (selected) { - setState(() { - _currentFilter = filter; - }); - }, - ), - ); - }).toList(), - ), - ); - } - - // 构建空状态 - Widget _buildEmptyState() { - final messages = { - TodoFilter.all: '还没有待办事项\n点击+号添加第一个任务', - TodoFilter.active: '没有待完成的任务\n享受轻松时光吧', - TodoFilter.completed: '还没有完成的任务\n加油哦!', - TodoFilter.today: '今天没有安排任务\n好好放松一下吧', - }; - - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.checklist, size: 80, color: Colors.grey.shade300), - const SizedBox(height: 20), - Text( - messages[_currentFilter] ?? '暂无数据', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, - color: Colors.grey.shade500, - ), - ), + Expanded(child: _buildActiveTodoList()), ], ), ); } - // 构建待办事项列表 - Widget _buildTodoList() { - return ListView.builder( - itemCount: _filteredTodos.length, - itemBuilder: (context, index) { - final todo = _filteredTodos[index]; - return _buildTodoItem(todo); - }, - ); - } - - // 构建单个待办事项项 - Widget _buildTodoItem(TodoItem todo) { - final isOverdue = todo.dueDate != null && - todo.dueDate!.isBefore(DateTime.now()) && - !todo.isCompleted; - - return Dismissible( - key: Key(todo.id), - direction: DismissDirection.endToStart, - background: Container( - color: Colors.red, - alignment: Alignment.centerRight, - padding: const EdgeInsets.only(right: 20), - child: const Icon(Icons.delete, color: Colors.white), - ), - confirmDismiss: (direction) async { - return await _showDeleteConfirmation(todo); - }, - onDismissed: (direction) => _deleteTodo(todo.id), - child: Card( - margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - color: todo.isCompleted ? Colors.grey.shade50 : null, - child: ListTile( - leading: Checkbox( - value: todo.isCompleted, - onChanged: (value) => _toggleTodo(todo.id), - ), - title: Text( - todo.title, - style: TextStyle( - decoration: todo.isCompleted ? TextDecoration.lineThrough : null, - color: todo.isCompleted ? Colors.grey : null, - fontWeight: FontWeight.w500, - ), - ), - subtitle: _buildTodoSubtitle(todo, isOverdue), - trailing: _buildPriorityBadge(todo.priority), - onTap: () => _showEditTodoDialog(todo), - onLongPress: () => _showTodoOptions(todo), - ), - ), - ); - } - - // 构建待办事项副标题 - Widget? _buildTodoSubtitle(TodoItem todo, bool isOverdue) { - final hasContent = todo.description?.isNotEmpty == true || - todo.dueDate != null; - - if (!hasContent) return null; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (todo.description?.isNotEmpty == true) - Text( - todo.description!, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 12, - color: Colors.grey.shade600, - ), - ), - if (todo.dueDate != null) - Text( - '截止: ${_formatDate(todo.dueDate!)}', - style: TextStyle( - fontSize: 11, - color: isOverdue ? Colors.red : Colors.grey, - fontWeight: isOverdue ? FontWeight.bold : FontWeight.normal, - ), - ), - ], - ); - } - - // 构建优先级徽章 - Widget _buildPriorityBadge(Priority priority) { - return Chip( - label: Text(priority.label), - backgroundColor: priority.color.withOpacity(0.1), - labelStyle: TextStyle( - color: priority.color, - fontSize: 10, - fontWeight: FontWeight.bold, - ), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - visualDensity: VisualDensity.compact, + Widget _buildActiveTodoList() { + return _activeTodos.isEmpty + ? buildEmptyState(_currentTab) + : buildTodoList( + todos: _activeTodos, + onToggleTodo: (todoId) { + setState(() { + _toggleTodo(todoId); + }); + }, + onEditTodo: (todo) { + _showEditTodoDialog(todo); + } ); } @@ -350,10 +81,12 @@ class _TodoPageState extends State { void _showAddTodoDialog() { _resetForm(); - showAwesomeDialog(context: context, - body: _buildTodoDialogContent(isEditing: true), - onOk: () => _saveTodo(false, null), - onCancel: () => _resetForm()); + showAwesomeDialog( + context: context, + body: _buildTodoDialogContent(isEditing: false), + onOk: () => _saveTodo(false, null), + onCancel: () => _resetForm(), + ); } // 显示编辑待办事项对话框 @@ -364,208 +97,57 @@ class _TodoPageState extends State { _selectedPriority = todo.priority; _selectedCategory = todo.category; - showAwesomeDialog(context: context, - body: _buildTodoDialogContent(isEditing: true), - onOk: () => _saveTodo(false, null), - onCancel: () => _resetForm()); + showAwesomeDialog( + context: context, + body: _buildTodoDialogContent(isEditing: true), + onOk: () => _saveTodo(true, todo), + onCancel: () => _resetForm(), + ); } - // 构建待办事项对话框 + // 构建待办事项对话框 - 现在使用新的组件 Widget _buildTodoDialogContent({bool isEditing = false}) { - var title = isEditing ? '编辑待办事项' : '添加待办事项'; - return Padding( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // 标题 - Row( - children: [ - Icon(Icons.add_task, color: Colors.orange, size: 24), - SizedBox(width: 8), - Text(title, style: TextStyle(fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.orange)), - ], - ), - SizedBox(height: 20), - - // 标题输入框 - TextField( - controller: _titleController, - decoration: InputDecoration( - labelText: '标题', - hintText: '请输入待办事项标题...', - counterText: '', - suffixText: '${_titleController.text.length}/20', - suffixStyle: TextStyle( - color: _titleController.text.length > 20 ? Colors.red : Colors.grey, - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: Colors.grey.shade200), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: Colors.grey.shade200), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: Colors.orange, width: 1), - ), - filled: true, - fillColor: Colors.white, - prefixIcon: Icon(Icons.title, color: Colors.black87), - contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), - ), - style: TextStyle(fontSize: 16), - maxLength: 20, - onChanged: (value) { - if (context.mounted) setState(() {}); - }, - ), - - SizedBox(height: 12), - - // 描述输入框 - TextField( - controller: _descriptionController, - decoration: InputDecoration( - labelText: '内容', - hintText: '请输入待办事项内容...', - counterText: '', - suffixText: '${_titleController.text.length}/50', - suffixStyle: TextStyle( - color: _titleController.text.length > 20 ? Colors.red : Colors.grey, - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: Colors.grey.shade200), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: Colors.grey.shade200), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: Colors.orange, width: 1), - ), - filled: true, - fillColor: Colors.white, - prefixIcon: Icon(Icons.description, color: Colors.black87), - contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), - ), - style: TextStyle(fontSize: 16), - maxLines: 2, - maxLength: 50 - ), - - SizedBox(height: 12), - - // 截止日期选择 - Container( - decoration: buildBoxDecoration(), - child: ListTile( - leading: Icon(Icons.calendar_today, color: Colors.blue), - title: Text( - _selectedDueDate == null - ? '选择截止日期' - : '截止: ${_formatDate(_selectedDueDate!)}', - style: TextStyle( - color: _selectedDueDate == null ? Colors.grey : Colors - .black87, - ), - ), - trailing: _selectedDueDate != null - ? IconButton( - icon: Icon(Icons.clear, size: 18), - onPressed: () { - setState(() => _selectedDueDate = null); - // 重新显示对话框以更新状态 - _showAddTodoDialog(); - }, - ) - : null, - onTap: () => _selectDueDateInDialog(), - ), - ), - SizedBox(height: 12), - - // 优先级选择 - Container( - decoration: buildBoxDecoration(), - child: ListTile( - leading: Container( - padding: EdgeInsets.all(6), - decoration: BoxDecoration( - color: _selectedPriority.color.withAlpha(10), - shape: BoxShape.circle, - ), - child: Icon( - Icons.flag, - color: _selectedPriority.color, - size: 18, - ), - ), - title: Text( - '优先级', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.grey.shade700, - ), - ), - trailing: DropdownButton( - value: _selectedPriority, - underline: SizedBox(), - icon: Icon(Icons.arrow_drop_down, color: Colors.grey.shade600), - iconSize: 20, - dropdownColor: Colors.white, - borderRadius: BorderRadius.circular(12), - onChanged: (value) { - setState(() => _selectedPriority = value!); - }, - items: Priority.values.map((priority) { - return DropdownMenuItem( - value: priority, - child: Container( - padding: EdgeInsets.symmetric(vertical: 8), - child: Row( - children: [ - Container( - padding: EdgeInsets.all(4), - decoration: buildBoxDecoration(), - child: Icon( - priority.icon, - color: priority.color, - size: 14, - ), - ), - SizedBox(width: 12), - Text( - priority.label, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - ); - }).toList(), - ), - contentPadding: EdgeInsets.symmetric(horizontal: 16), - minLeadingWidth: 0, - ), - ), - ], - ), + return TodoDialogContent( + isEditing: isEditing, + titleController: _titleController, + descriptionController: _descriptionController, + selectedDueDate: _selectedDueDate, + selectedPriority: _selectedPriority, + selectedCategory: _selectedCategory, + onDueDateChanged: (date) { + setState(() { + _selectedDueDate = date; + }); + }, + onPriorityChanged: (priority) { + setState(() { + _selectedPriority = priority; + }); + }, + onCategoryChanged: (category) { + setState(() { + _selectedCategory = category; + }); + }, + onClearDueDate: () { + setState(() { + _selectedDueDate = null; + }); + // 重新显示对话框以更新状态 + if (isEditing) { + _showEditTodoDialog(_todos.firstWhere((todo) => + _titleController.text == todo.title && + _descriptionController.text == (todo.description ?? ''))); + } else { + _showAddTodoDialog(); + } + }, + onSelectDueDate: () => _selectDueDateInDialog(isEditing), ); } // 在对话框中选择日期 - Future _selectDueDateInDialog() async { + Future _selectDueDateInDialog(bool isEditing) async { final DateTime? picked = await showDatePicker( context: context, initialDate: DateTime.now(), @@ -578,15 +160,21 @@ class _TodoPageState extends State { _selectedDueDate = picked; }); // 重新显示对话框以更新日期显示 - _showAddTodoDialog(); + if (isEditing) { + // 找到对应的待办事项重新显示编辑对话框 + final todo = _todos.firstWhere((todo) => + _titleController.text == todo.title && + _descriptionController.text == (todo.description ?? '')); + _showEditTodoDialog(todo); + } else { + _showAddTodoDialog(); + } } } // 验证表单 bool _validateForm() { - return _titleController.text - .trim() - .isNotEmpty; + return _titleController.text.trim().isNotEmpty; } // 重置表单 @@ -594,7 +182,7 @@ class _TodoPageState extends State { _titleController.clear(); _descriptionController.clear(); _selectedDueDate = null; - _selectedPriority = Priority.medium; + _selectedPriority = TodoPriority.medium; _selectedCategory = null; } @@ -603,15 +191,11 @@ class _TodoPageState extends State { if (!_validateForm()) return; final newTodo = TodoItem( - id: isEditing ? todo!.id : DateTime - .now() - .millisecondsSinceEpoch - .toString(), + id: isEditing ? todo!.id : DateTime.now().millisecondsSinceEpoch.toString(), title: _titleController.text.trim(), - description: _descriptionController.text - .trim() - .isEmpty ? - null : _descriptionController.text.trim(), + description: _descriptionController.text.trim().isEmpty + ? null + : _descriptionController.text.trim(), dueDate: _selectedDueDate, priority: _selectedPriority, category: _selectedCategory, @@ -621,8 +205,9 @@ class _TodoPageState extends State { if (isEditing) { final index = _todos.indexWhere((t) => t.id == todo!.id); if (index != -1) { - _todos[index] = - newTodo.copyWith(isCompleted: _todos[index].isCompleted); + _todos[index] = newTodo.copyWith( + isCompleted: _todos[index].isCompleted, + ); } } else { _todos.insert(0, newTodo); @@ -645,7 +230,7 @@ class _TodoPageState extends State { btnOkText: "好的", btnOkColor: Colors.green, btnOkOnPress: () {}, - autoHide: Duration(seconds: 2), // 2秒后自动关闭 + autoHide: Duration(seconds: 2), ).show(); } @@ -661,151 +246,6 @@ class _TodoPageState extends State { }); } - // 显示删除确认 - Future _showDeleteConfirmation(TodoItem todo) async { - return await showDialog( - context: context, - builder: (context) => - AlertDialog( - title: const Text('确认删除'), - content: Text('确定要删除"${todo.title}"吗?此操作不可撤销。'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: const Text('取消'), - ), - TextButton( - onPressed: () => Navigator.pop(context, true), - child: const Text('删除', style: TextStyle(color: Colors.red)), - ), - ], - ), - ) ?? false; - } - - // 删除待办事项 - void _deleteTodo(String id) { - setState(() { - _todos.removeWhere((todo) => todo.id == id); - }); - - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('待办事项已删除')), - ); - } - - // 显示待办事项选项 - void _showTodoOptions(TodoItem todo) { - showModalBottomSheet( - context: context, - builder: (context) => - Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - leading: const Icon(Icons.content_copy), - title: const Text('复制内容'), - onTap: () { - Navigator.pop(context); - _copyTodoContent(todo); - }, - ), - ListTile( - leading: Icon(todo.isCompleted ? Icons.undo : Icons.done_all), - title: Text(todo.isCompleted ? '标记为未完成' : '标记为已完成'), - onTap: () { - Navigator.pop(context); - _toggleTodo(todo.id); - }, - ), - const Divider(), - ListTile( - leading: const Icon(Icons.delete, color: Colors.red), - title: const Text('删除', style: TextStyle(color: Colors.red)), - onTap: () { - Navigator.pop(context); - _deleteTodo(todo.id); - }, - ), - ], - ), - ); - } - - // 复制待办事项内容 - void _copyTodoContent(TodoItem todo) { - final content = '${todo.title}\n${todo.description ?? ''}'.trim(); - Clipboard.setData(ClipboardData(text: content)); - - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('内容已复制到剪贴板')), - ); - } - - // 清除已完成的任务 - void _clearCompleted() { - setState(() { - _todos.removeWhere((todo) => todo.isCompleted); - }); - - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('已清除所有已完成的任务')), - ); - } - - // 处理菜单操作 - void _handleMenuAction(String value) { - switch (value) { - case 'export': - _exportTodos(); - break; - case 'sort': - _sortTodos(); - break; - } - } - - // 导出待办事项 - void _exportTodos() { - final exportText = _todos.map((todo) { - return '${todo.isCompleted ? '[✓]' : '[ ]'} ${todo.title}${todo.dueDate != - null ? ' (截止: ${_formatDate(todo.dueDate!)})' : ''}'; - }).join('\n'); - - Clipboard.setData(ClipboardData(text: exportText)); - - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('待办事项已导出到剪贴板')), - ); - } - - // 排序待办事项 - void _sortTodos() { - setState(() { - _todos.sort((a, b) { - // 按优先级排序(高优先级在前) - if (a.priority.index != b.priority.index) { - return b.priority.index.compareTo(a.priority.index); - } - // 按截止日期排序(有截止日期的在前,然后按时间顺序) - if (a.dueDate != null && b.dueDate != null) { - return a.dueDate!.compareTo(b.dueDate!); - } else if (a.dueDate != null) { - return -1; - } else if (b.dueDate != null) { - return 1; - } - // 按创建时间排序(新的在前) - return b.createdAt.compareTo(a.createdAt); - }); - }); - } - - // 格式化日期 - String _formatDate(DateTime date) { - return '${date.month}月${date.day}日'; - } - @override void dispose() { _titleController.dispose(); diff --git a/lib/store/todo_dialog_store.dart b/lib/store/todo_dialog_store.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/utils/date_utils.dart b/lib/utils/date_utils.dart new file mode 100644 index 0000000..d92aa43 --- /dev/null +++ b/lib/utils/date_utils.dart @@ -0,0 +1,3 @@ +String formatDate(DateTime date) { + return '${date.month}月${date.day}日'; +} \ No newline at end of file diff --git a/lib/utils/todo_utils.dart b/lib/utils/todo_utils.dart new file mode 100644 index 0000000..49d8d86 --- /dev/null +++ b/lib/utils/todo_utils.dart @@ -0,0 +1,23 @@ +import 'package:flisp_app/models/todo.dart'; + +List getActiveTodos(TodoTab currentTab, List todos) { + switch (currentTab) { + case TodoTab.active: + return todos.where((todo) => !todo.isCompleted).toList(); + case TodoTab.completed: + return todos.where((todo) => todo.isCompleted).toList(); + case TodoTab.today: + final today = DateTime.now(); + return todos + .where( + (todo) => + todo.dueDate != null && + todo.dueDate!.year == today.year && + todo.dueDate!.month == today.month && + todo.dueDate!.day == today.day, + ) + .toList(); + default: + return todos; + } +} \ No newline at end of file diff --git a/lib/models/common.dart b/lib/widgets/common.dart similarity index 93% rename from lib/models/common.dart rename to lib/widgets/common.dart index 8d3e635..2ac30a2 100644 --- a/lib/models/common.dart +++ b/lib/widgets/common.dart @@ -12,8 +12,8 @@ BoxDecoration buildBoxDecoration() { Container buildBody({Widget? child}) { return Container( color: Colors.grey[50], - padding: EdgeInsets.all(5), - child: SingleChildScrollView(child: child), + padding: EdgeInsets.all(8), + child: child, ); } diff --git a/lib/widgets/todo_dialog.dart b/lib/widgets/todo_dialog.dart new file mode 100644 index 0000000..3664724 --- /dev/null +++ b/lib/widgets/todo_dialog.dart @@ -0,0 +1,246 @@ +import 'package:flutter/material.dart'; +import 'package:flisp_app/models/todo.dart'; +import 'package:flisp_app/utils/date_utils.dart'; +import 'package:flisp_app/widgets/common.dart'; + +class TodoDialogContent extends StatefulWidget { + final bool isEditing; + final TextEditingController titleController; + final TextEditingController descriptionController; + final DateTime? selectedDueDate; + final TodoPriority selectedPriority; + final String? selectedCategory; + final Function(DateTime?) onDueDateChanged; + final Function(TodoPriority) onPriorityChanged; + final Function(String?) onCategoryChanged; + final VoidCallback onClearDueDate; + final VoidCallback onSelectDueDate; + + const TodoDialogContent({ + super.key, + required this.isEditing, + required this.titleController, + required this.descriptionController, + required this.selectedDueDate, + required this.selectedPriority, + required this.selectedCategory, + required this.onDueDateChanged, + required this.onPriorityChanged, + required this.onCategoryChanged, + required this.onClearDueDate, + required this.onSelectDueDate, + }); + + @override + State createState() => _TodoDialogContentState(); +} + +class _TodoDialogContentState extends State { + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 标题 + Row( + children: [ + Icon(Icons.add_task, color: Colors.orange, size: 24), + SizedBox(width: 8), + Text( + widget.isEditing ? '编辑待办事项' : '添加待办事项', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.orange, + ), + ), + ], + ), + SizedBox(height: 20), + + // 标题输入框 + TextField( + controller: widget.titleController, + decoration: InputDecoration( + labelText: '标题', + hintText: '请输入待办事项标题...', + counterText: '', + suffixText: '${widget.titleController.text.length}/20', + suffixStyle: TextStyle( + color: widget.titleController.text.length > 20 + ? Colors.red + : Colors.grey, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade200), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade200), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.orange, width: 1), + ), + filled: true, + fillColor: Colors.white, + prefixIcon: Icon(Icons.title, color: Colors.black87), + contentPadding: EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + ), + style: TextStyle(fontSize: 16), + maxLength: 20, + onChanged: (value) { + if (mounted) setState(() {}); + }, + ), + + SizedBox(height: 12), + + // 描述输入框 + TextField( + controller: widget.descriptionController, + decoration: InputDecoration( + labelText: '内容', + hintText: '请输入待办事项内容...', + counterText: '', + suffixText: '${widget.titleController.text.length}/50', + suffixStyle: TextStyle( + color: widget.titleController.text.length > 20 + ? Colors.red + : Colors.grey, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade200), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade200), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.orange, width: 1), + ), + filled: true, + fillColor: Colors.white, + prefixIcon: Icon(Icons.description, color: Colors.black87), + contentPadding: EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + ), + style: TextStyle(fontSize: 16), + maxLines: 2, + maxLength: 50, + ), + + SizedBox(height: 12), + + // 截止日期选择 + Container( + decoration: buildBoxDecoration(), + child: ListTile( + leading: Icon(Icons.calendar_today, color: Colors.blue), + title: Text( + widget.selectedDueDate == null + ? '选择截止日期' + : '截止: ${formatDate(widget.selectedDueDate!)}', + style: TextStyle( + color: widget.selectedDueDate == null + ? Colors.grey + : Colors.black87, + ), + ), + trailing: widget.selectedDueDate != null + ? IconButton( + icon: Icon(Icons.clear, size: 18), + onPressed: widget.onClearDueDate, + ) + : null, + onTap: widget.onSelectDueDate, + ), + ), + SizedBox(height: 12), + + // 优先级选择 + Container( + decoration: buildBoxDecoration(), + child: ListTile( + leading: Container( + padding: EdgeInsets.all(6), + decoration: BoxDecoration( + color: widget.selectedPriority.color.withAlpha(10), + shape: BoxShape.circle, + ), + child: Icon( + Icons.flag, + color: widget.selectedPriority.color, + size: 18, + ), + ), + title: Text( + '优先级', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.grey.shade700, + ), + ), + trailing: DropdownButton( + value: widget.selectedPriority, + underline: SizedBox(), + icon: Icon(Icons.arrow_drop_down, color: Colors.grey.shade600), + iconSize: 20, + dropdownColor: Colors.white, + borderRadius: BorderRadius.circular(12), + onChanged: (value) { + if (value != null) { + widget.onPriorityChanged(value); + if (mounted) setState(() {}); + } + }, + items: TodoPriority.values.map((priority) { + return DropdownMenuItem( + value: priority, + child: Container( + padding: EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + Container( + padding: EdgeInsets.all(4), + decoration: buildBoxDecoration(), + child: Icon( + priority.icon, + color: priority.color, + size: 14, + ), + ), + SizedBox(width: 12), + Text( + priority.label, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ); + }).toList(), + ), + contentPadding: EdgeInsets.symmetric(horizontal: 16), + minLeadingWidth: 0, + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/todo_widget.dart b/lib/widgets/todo_widget.dart new file mode 100644 index 0000000..93dd613 --- /dev/null +++ b/lib/widgets/todo_widget.dart @@ -0,0 +1,200 @@ +import 'package:flisp_app/models/todo.dart'; +import 'package:flisp_app/utils/date_utils.dart'; +import 'package:flisp_app/widgets/common.dart'; +import 'package:flutter/material.dart'; +import 'package:toggle_switch/toggle_switch.dart'; + +// 统计卡片 +Widget buildStatsCard(List todos) { + int totalCount = todos.length; + + int activeCount = todos.where((todo) => !todo.isCompleted).length; + + int completedCount = todos.where((todo) => todo.isCompleted).length; + + return buildCard( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildStatItem('总计', totalCount, Colors.blue), + _buildStatItem('待完成', activeCount, Colors.orange), + _buildStatItem('已完成', completedCount, Colors.green), + ], + ), + ); +} + +Widget _buildStatItem(String label, int count, Color color) { + return Column( + children: [ + Text( + count.toString(), + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: color, + ), + ), + const SizedBox(height: 4), + Text(label, style: const TextStyle(fontSize: 12, color: Colors.grey)), + ], + ); +} + +// Tab页 +Widget buildTabs({ + required TodoTab currentTab, + required ValueChanged onTabChanged, +}) { + return Container( + padding: EdgeInsets.all(8), + child: ToggleSwitch( + minWidth: 90.0, + minHeight: 40.0, + initialLabelIndex: TodoTab.values.indexOf(currentTab), + totalSwitches: TodoTab.values.length, + labels: TodoTab.values.map((e) => e.label).toList(), + activeBgColor: [Colors.orange.shade600], + activeFgColor: Colors.white, + inactiveBgColor: Colors.grey.shade200, + inactiveFgColor: Colors.grey.shade700, + cornerRadius: 12.0, + customTextStyles: [TextStyle(fontSize: 12, fontWeight: FontWeight.w500)], + onToggle: (index) { + if (index != null) { + onTabChanged(TodoTab.values[index]); + } + }, + ), + ); +} + +Widget buildTodoList({ + required List todos, + required ValueChanged onToggleTodo, + required ValueChanged onEditTodo +}) { + return ListView.separated( + itemCount: todos.length, + separatorBuilder: (context, index) => SizedBox(height: 8), + itemBuilder: (context, index) { + final todo = todos[index]; + return _buildTodoItem( + todo: todo, + onToggle: onToggleTodo, + onEdit: onEditTodo + ); + }, + ); +} + +Widget _buildTodoItem({ + required TodoItem todo, + required ValueChanged onToggle, + required ValueChanged onEdit +}) { + return buildCard( + child: ListTile( + leading: Checkbox( + value: todo.isCompleted, + onChanged: (value) => onToggle(todo.id), + ), + title: _buildTodoTitle(todo), + subtitle: _buildTodoSubtitle(todo), + trailing: _buildPriorityBadge(todo.priority), + onTap: () => onEdit(todo), + // onLongPress: () => onShowOptions(todo), + ), + ); +} + +Widget _buildTodoTitle(TodoItem todo) { + return Text( + todo.title, + style: TextStyle( + decoration: todo.isCompleted ? TextDecoration.lineThrough : null, + color: todo.isCompleted ? Colors.grey : null, + fontWeight: FontWeight.w500, + ), + ); +} + +// 构建待办事项副标题 +Widget? _buildTodoSubtitle(TodoItem todo) { + final isOverdue = + todo.dueDate != null && + todo.dueDate!.isBefore(DateTime.now()) && + !todo.isCompleted; + + final hasContent = + todo.description?.isNotEmpty == true || todo.dueDate != null; + + if (!hasContent) return null; + + return Wrap( + direction: Axis.vertical, + spacing: 5, + children: [ + if (todo.description?.isNotEmpty == true) + Text( + todo.description!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 12, color: Colors.grey.shade600), + ), + if (todo.dueDate != null) + Text( + '截止: ${formatDate(todo.dueDate!)}', + style: TextStyle( + fontSize: 11, + color: isOverdue ? Colors.red : Colors.grey, + fontWeight: isOverdue ? FontWeight.bold : FontWeight.normal, + ), + ), + ], + ); +} + +// 构建优先级徽章 +Widget _buildPriorityBadge(TodoPriority priority) { + return Chip( + label: Text(priority.label), + backgroundColor: priority.color, + labelStyle: TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide(color: Colors.white), + ), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + visualDensity: VisualDensity.compact, + ); +} + +// 空状态 +Widget buildEmptyState(TodoTab currentFilter) { + final messages = { + TodoTab.all: '📝 还没有待办事项\n点击➕号添加第一个任务吧~', + TodoTab.active: '🎯 没有待完成的任务\n享受轻松时光吧!✨', + TodoTab.completed: '🎉 还没有完成的任务\n加油哦!💪', + TodoTab.today: '📅 今天没有安排任务\n好好放松一下吧~😊', + }; + + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.checklist, size: 80, color: Colors.orange.shade600), + const SizedBox(height: 20), + Text( + messages[currentFilter] ?? '暂无数据', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 16, color: Colors.orange.shade600), + ), + ], + ), + ); +} diff --git a/pubspec.lock b/pubspec.lock index 2376400..4b19811 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -102,6 +102,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "5.0.0" + flutter_riverpod: + dependency: "direct main" + description: + name: flutter_riverpod + sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.6.1" flutter_test: dependency: "direct dev" description: flutter @@ -272,6 +280,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.0.16" + riverpod: + dependency: transitive + description: + name: riverpod + sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.6.1" shared_preferences: dependency: "direct main" description: @@ -349,6 +365,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.12.1" + state_notifier: + dependency: transitive + description: + name: state_notifier + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.0" stream_channel: dependency: transitive description: @@ -381,6 +405,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.7.4" + toggle_switch: + dependency: "direct main" + description: + name: toggle_switch + sha256: dca04512d7c23ed320d6c5ede1211a404f177d54d353bf785b07d15546a86ce5 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.0" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2033182..63c07c3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,6 +37,8 @@ dependencies: provider: ^6.1.1 # 状态管理 shared_preferences: ^2.2.2 # 本地存储 awesome_dialog: ^3.3.0 + toggle_switch: ^2.3.0 + flutter_riverpod: ^2.4.9 dev_dependencies: flutter_test: