feat:重构代办事项模块
This commit is contained in:
@@ -7,29 +7,29 @@ import 'package:flisp_app/provider/app_provider.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class MainScreen extends StatelessWidget {
|
class MainScreen extends StatefulWidget {
|
||||||
const MainScreen({super.key});
|
const MainScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MainScreen> createState() => _MainScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MainScreenState extends State<MainScreen> {
|
||||||
|
final GlobalKey<TodoPageState> _todoPageKey = GlobalKey();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer<AppProvider>(
|
return Consumer<AppProvider>(
|
||||||
builder: (context, appState, child) {
|
builder: (context, appState, child) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
// 侧边抽屉菜单
|
|
||||||
drawer: const AppDrawer(),
|
drawer: const AppDrawer(),
|
||||||
|
|
||||||
// 顶部导航栏
|
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('闪灵'),
|
title: Text(_getAppBarTitle(appState.currentIndex)),
|
||||||
backgroundColor: Colors.blue,
|
backgroundColor: Colors.blue,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
),
|
),
|
||||||
|
|
||||||
// 主体内容
|
|
||||||
body: _buildPage(appState.currentIndex),
|
body: _buildPage(appState.currentIndex),
|
||||||
|
|
||||||
// 底部导航栏
|
|
||||||
bottomNavigationBar: BottomNavigationBar(
|
bottomNavigationBar: BottomNavigationBar(
|
||||||
currentIndex: appState.currentIndex,
|
currentIndex: appState.currentIndex,
|
||||||
onTap: (index) => appState.changeTab(index),
|
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) {
|
Widget _buildPage(int index) {
|
||||||
switch (index) {
|
switch (index) {
|
||||||
case 0:
|
case 0:
|
||||||
@@ -66,11 +104,21 @@ class MainScreen extends StatelessWidget {
|
|||||||
case 1:
|
case 1:
|
||||||
return const NotesPage();
|
return const NotesPage();
|
||||||
case 2:
|
case 2:
|
||||||
return const TodoPage();
|
return TodoPage(key: _todoPageKey); // 传递 key
|
||||||
case 3:
|
case 3:
|
||||||
return const RemindersPage();
|
return const RemindersPage();
|
||||||
default:
|
default:
|
||||||
return const FlashPage();
|
return const FlashPage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _getAppBarTitle(int index) {
|
||||||
|
final titles = {
|
||||||
|
0: '闪灵',
|
||||||
|
1: '笔记',
|
||||||
|
2: '待办事项',
|
||||||
|
3: '提醒任务',
|
||||||
|
};
|
||||||
|
return titles[index] ?? '闪灵';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
68
lib/models/todo.dart
Normal file
68
lib/models/todo.dart
Normal file
@@ -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);
|
||||||
|
}
|
||||||
@@ -1,348 +1,79 @@
|
|||||||
import 'package:awesome_dialog/awesome_dialog.dart';
|
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/material.dart';
|
||||||
import 'package:flutter/services.dart'; // 用于复制功能
|
|
||||||
|
|
||||||
class TodoPage extends StatefulWidget {
|
class TodoPage extends StatefulWidget {
|
||||||
const TodoPage({super.key});
|
final VoidCallback? onAddTodoPressed;
|
||||||
|
|
||||||
|
const TodoPage({super.key, this.onAddTodoPressed});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<TodoPage> createState() => _TodoPageState();
|
State<TodoPage> createState() => TodoPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 待办事项数据模型
|
class TodoPageState extends State<TodoPage> {
|
||||||
class TodoItem {
|
// 添加一个公共方法供外部调用
|
||||||
String id;
|
void showAddTodoDialog() {
|
||||||
String title;
|
_showAddTodoDialog();
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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<TodoPage> {
|
|
||||||
// 待办事项列表
|
// 待办事项列表
|
||||||
List<TodoItem> _todos = [];
|
final List<TodoItem> _todos = [
|
||||||
TodoFilter _currentFilter = TodoFilter.all;
|
// ... 原有的待办事项数据保持不变
|
||||||
|
];
|
||||||
|
|
||||||
|
TodoTab _currentTab = TodoTab.all;
|
||||||
final TextEditingController _titleController = TextEditingController();
|
final TextEditingController _titleController = TextEditingController();
|
||||||
final TextEditingController _descriptionController = TextEditingController();
|
final TextEditingController _descriptionController = TextEditingController();
|
||||||
DateTime? _selectedDueDate;
|
DateTime? _selectedDueDate;
|
||||||
Priority _selectedPriority = Priority.medium;
|
TodoPriority _selectedPriority = TodoPriority.medium;
|
||||||
String? _selectedCategory;
|
String? _selectedCategory;
|
||||||
|
|
||||||
// 获取过滤后的待办事项
|
// 获取过滤后的待办事项
|
||||||
List<TodoItem> get _filteredTodos {
|
List<TodoItem> get _activeTodos {
|
||||||
switch (_currentFilter) {
|
return getActiveTodos(_currentTab, _todos);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 统计数据
|
|
||||||
int get _totalCount => _todos.length;
|
|
||||||
|
|
||||||
int get _activeCount =>
|
|
||||||
_todos
|
|
||||||
.where((todo) => !todo.isCompleted)
|
|
||||||
.length;
|
|
||||||
|
|
||||||
int get _completedCount =>
|
|
||||||
_todos
|
|
||||||
.where((todo) => todo.isCompleted)
|
|
||||||
.length;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return buildBody(
|
||||||
appBar: AppBar(
|
child: Column(
|
||||||
title: const Text('待办事项'),
|
|
||||||
backgroundColor: Colors.orange,
|
|
||||||
foregroundColor: Colors.white
|
|
||||||
),
|
|
||||||
body: Column(
|
|
||||||
children: [
|
children: [
|
||||||
// 统计信息卡片
|
// 统计信息卡片
|
||||||
_buildStatsCard(),
|
buildStatsCard(_todos),
|
||||||
|
// 选项卡
|
||||||
// 过滤器选项卡
|
buildTabs(
|
||||||
_buildFilterTabs(),
|
currentTab: _currentTab,
|
||||||
|
onTabChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_currentTab = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
// 待办事项列表
|
// 待办事项列表
|
||||||
Expanded(
|
Expanded(child: _buildActiveTodoList()),
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建待办事项列表
|
Widget _buildActiveTodoList() {
|
||||||
Widget _buildTodoList() {
|
return _activeTodos.isEmpty
|
||||||
return ListView.builder(
|
? buildEmptyState(_currentTab)
|
||||||
itemCount: _filteredTodos.length,
|
: buildTodoList(
|
||||||
itemBuilder: (context, index) {
|
todos: _activeTodos,
|
||||||
final todo = _filteredTodos[index];
|
onToggleTodo: (todoId) {
|
||||||
return _buildTodoItem(todo);
|
setState(() {
|
||||||
},
|
_toggleTodo(todoId);
|
||||||
);
|
});
|
||||||
}
|
},
|
||||||
|
onEditTodo: (todo) {
|
||||||
// 构建单个待办事项项
|
_showEditTodoDialog(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,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,10 +81,12 @@ class _TodoPageState extends State<TodoPage> {
|
|||||||
void _showAddTodoDialog() {
|
void _showAddTodoDialog() {
|
||||||
_resetForm();
|
_resetForm();
|
||||||
|
|
||||||
showAwesomeDialog(context: context,
|
showAwesomeDialog(
|
||||||
body: _buildTodoDialogContent(isEditing: true),
|
context: context,
|
||||||
onOk: () => _saveTodo(false, null),
|
body: _buildTodoDialogContent(isEditing: false),
|
||||||
onCancel: () => _resetForm());
|
onOk: () => _saveTodo(false, null),
|
||||||
|
onCancel: () => _resetForm(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示编辑待办事项对话框
|
// 显示编辑待办事项对话框
|
||||||
@@ -364,208 +97,57 @@ class _TodoPageState extends State<TodoPage> {
|
|||||||
_selectedPriority = todo.priority;
|
_selectedPriority = todo.priority;
|
||||||
_selectedCategory = todo.category;
|
_selectedCategory = todo.category;
|
||||||
|
|
||||||
showAwesomeDialog(context: context,
|
showAwesomeDialog(
|
||||||
body: _buildTodoDialogContent(isEditing: true),
|
context: context,
|
||||||
onOk: () => _saveTodo(false, null),
|
body: _buildTodoDialogContent(isEditing: true),
|
||||||
onCancel: () => _resetForm());
|
onOk: () => _saveTodo(true, todo),
|
||||||
|
onCancel: () => _resetForm(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建待办事项对话框
|
// 构建待办事项对话框 - 现在使用新的组件
|
||||||
Widget _buildTodoDialogContent({bool isEditing = false}) {
|
Widget _buildTodoDialogContent({bool isEditing = false}) {
|
||||||
var title = isEditing ? '编辑待办事项' : '添加待办事项';
|
return TodoDialogContent(
|
||||||
return Padding(
|
isEditing: isEditing,
|
||||||
padding: const EdgeInsets.all(16),
|
titleController: _titleController,
|
||||||
child: Column(
|
descriptionController: _descriptionController,
|
||||||
mainAxisSize: MainAxisSize.min,
|
selectedDueDate: _selectedDueDate,
|
||||||
children: [
|
selectedPriority: _selectedPriority,
|
||||||
// 标题
|
selectedCategory: _selectedCategory,
|
||||||
Row(
|
onDueDateChanged: (date) {
|
||||||
children: [
|
setState(() {
|
||||||
Icon(Icons.add_task, color: Colors.orange, size: 24),
|
_selectedDueDate = date;
|
||||||
SizedBox(width: 8),
|
});
|
||||||
Text(title, style: TextStyle(fontSize: 20,
|
},
|
||||||
fontWeight: FontWeight.bold,
|
onPriorityChanged: (priority) {
|
||||||
color: Colors.orange)),
|
setState(() {
|
||||||
],
|
_selectedPriority = priority;
|
||||||
),
|
});
|
||||||
SizedBox(height: 20),
|
},
|
||||||
|
onCategoryChanged: (category) {
|
||||||
// 标题输入框
|
setState(() {
|
||||||
TextField(
|
_selectedCategory = category;
|
||||||
controller: _titleController,
|
});
|
||||||
decoration: InputDecoration(
|
},
|
||||||
labelText: '标题',
|
onClearDueDate: () {
|
||||||
hintText: '请输入待办事项标题...',
|
setState(() {
|
||||||
counterText: '',
|
_selectedDueDate = null;
|
||||||
suffixText: '${_titleController.text.length}/20',
|
});
|
||||||
suffixStyle: TextStyle(
|
// 重新显示对话框以更新状态
|
||||||
color: _titleController.text.length > 20 ? Colors.red : Colors.grey,
|
if (isEditing) {
|
||||||
),
|
_showEditTodoDialog(_todos.firstWhere((todo) =>
|
||||||
border: OutlineInputBorder(
|
_titleController.text == todo.title &&
|
||||||
borderRadius: BorderRadius.circular(12),
|
_descriptionController.text == (todo.description ?? '')));
|
||||||
borderSide: BorderSide(color: Colors.grey.shade200),
|
} else {
|
||||||
),
|
_showAddTodoDialog();
|
||||||
enabledBorder: OutlineInputBorder(
|
}
|
||||||
borderRadius: BorderRadius.circular(12),
|
},
|
||||||
borderSide: BorderSide(color: Colors.grey.shade200),
|
onSelectDueDate: () => _selectDueDateInDialog(isEditing),
|
||||||
),
|
|
||||||
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<Priority>(
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在对话框中选择日期
|
// 在对话框中选择日期
|
||||||
Future<void> _selectDueDateInDialog() async {
|
Future<void> _selectDueDateInDialog(bool isEditing) async {
|
||||||
final DateTime? picked = await showDatePicker(
|
final DateTime? picked = await showDatePicker(
|
||||||
context: context,
|
context: context,
|
||||||
initialDate: DateTime.now(),
|
initialDate: DateTime.now(),
|
||||||
@@ -578,15 +160,21 @@ class _TodoPageState extends State<TodoPage> {
|
|||||||
_selectedDueDate = picked;
|
_selectedDueDate = picked;
|
||||||
});
|
});
|
||||||
// 重新显示对话框以更新日期显示
|
// 重新显示对话框以更新日期显示
|
||||||
_showAddTodoDialog();
|
if (isEditing) {
|
||||||
|
// 找到对应的待办事项重新显示编辑对话框
|
||||||
|
final todo = _todos.firstWhere((todo) =>
|
||||||
|
_titleController.text == todo.title &&
|
||||||
|
_descriptionController.text == (todo.description ?? ''));
|
||||||
|
_showEditTodoDialog(todo);
|
||||||
|
} else {
|
||||||
|
_showAddTodoDialog();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证表单
|
// 验证表单
|
||||||
bool _validateForm() {
|
bool _validateForm() {
|
||||||
return _titleController.text
|
return _titleController.text.trim().isNotEmpty;
|
||||||
.trim()
|
|
||||||
.isNotEmpty;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重置表单
|
// 重置表单
|
||||||
@@ -594,7 +182,7 @@ class _TodoPageState extends State<TodoPage> {
|
|||||||
_titleController.clear();
|
_titleController.clear();
|
||||||
_descriptionController.clear();
|
_descriptionController.clear();
|
||||||
_selectedDueDate = null;
|
_selectedDueDate = null;
|
||||||
_selectedPriority = Priority.medium;
|
_selectedPriority = TodoPriority.medium;
|
||||||
_selectedCategory = null;
|
_selectedCategory = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -603,15 +191,11 @@ class _TodoPageState extends State<TodoPage> {
|
|||||||
if (!_validateForm()) return;
|
if (!_validateForm()) return;
|
||||||
|
|
||||||
final newTodo = TodoItem(
|
final newTodo = TodoItem(
|
||||||
id: isEditing ? todo!.id : DateTime
|
id: isEditing ? todo!.id : DateTime.now().millisecondsSinceEpoch.toString(),
|
||||||
.now()
|
|
||||||
.millisecondsSinceEpoch
|
|
||||||
.toString(),
|
|
||||||
title: _titleController.text.trim(),
|
title: _titleController.text.trim(),
|
||||||
description: _descriptionController.text
|
description: _descriptionController.text.trim().isEmpty
|
||||||
.trim()
|
? null
|
||||||
.isEmpty ?
|
: _descriptionController.text.trim(),
|
||||||
null : _descriptionController.text.trim(),
|
|
||||||
dueDate: _selectedDueDate,
|
dueDate: _selectedDueDate,
|
||||||
priority: _selectedPriority,
|
priority: _selectedPriority,
|
||||||
category: _selectedCategory,
|
category: _selectedCategory,
|
||||||
@@ -621,8 +205,9 @@ class _TodoPageState extends State<TodoPage> {
|
|||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
final index = _todos.indexWhere((t) => t.id == todo!.id);
|
final index = _todos.indexWhere((t) => t.id == todo!.id);
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
_todos[index] =
|
_todos[index] = newTodo.copyWith(
|
||||||
newTodo.copyWith(isCompleted: _todos[index].isCompleted);
|
isCompleted: _todos[index].isCompleted,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_todos.insert(0, newTodo);
|
_todos.insert(0, newTodo);
|
||||||
@@ -645,7 +230,7 @@ class _TodoPageState extends State<TodoPage> {
|
|||||||
btnOkText: "好的",
|
btnOkText: "好的",
|
||||||
btnOkColor: Colors.green,
|
btnOkColor: Colors.green,
|
||||||
btnOkOnPress: () {},
|
btnOkOnPress: () {},
|
||||||
autoHide: Duration(seconds: 2), // 2秒后自动关闭
|
autoHide: Duration(seconds: 2),
|
||||||
).show();
|
).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -661,151 +246,6 @@ class _TodoPageState extends State<TodoPage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示删除确认
|
|
||||||
Future<bool> _showDeleteConfirmation(TodoItem todo) async {
|
|
||||||
return await showDialog<bool>(
|
|
||||||
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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_titleController.dispose();
|
_titleController.dispose();
|
||||||
|
|||||||
0
lib/store/todo_dialog_store.dart
Normal file
0
lib/store/todo_dialog_store.dart
Normal file
3
lib/utils/date_utils.dart
Normal file
3
lib/utils/date_utils.dart
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
String formatDate(DateTime date) {
|
||||||
|
return '${date.month}月${date.day}日';
|
||||||
|
}
|
||||||
23
lib/utils/todo_utils.dart
Normal file
23
lib/utils/todo_utils.dart
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import 'package:flisp_app/models/todo.dart';
|
||||||
|
|
||||||
|
List<TodoItem> getActiveTodos(TodoTab currentTab, List<TodoItem> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,8 +12,8 @@ BoxDecoration buildBoxDecoration() {
|
|||||||
Container buildBody({Widget? child}) {
|
Container buildBody({Widget? child}) {
|
||||||
return Container(
|
return Container(
|
||||||
color: Colors.grey[50],
|
color: Colors.grey[50],
|
||||||
padding: EdgeInsets.all(5),
|
padding: EdgeInsets.all(8),
|
||||||
child: SingleChildScrollView(child: child),
|
child: child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
246
lib/widgets/todo_dialog.dart
Normal file
246
lib/widgets/todo_dialog.dart
Normal file
@@ -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<TodoDialogContent> createState() => _TodoDialogContentState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TodoDialogContentState extends State<TodoDialogContent> {
|
||||||
|
@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<TodoPriority>(
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
200
lib/widgets/todo_widget.dart
Normal file
200
lib/widgets/todo_widget.dart
Normal file
@@ -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<TodoItem> 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<TodoTab> 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<TodoItem> todos,
|
||||||
|
required ValueChanged<String> onToggleTodo,
|
||||||
|
required ValueChanged<TodoItem> 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<String> onToggle,
|
||||||
|
required ValueChanged<TodoItem> 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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
32
pubspec.lock
32
pubspec.lock
@@ -102,6 +102,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.0"
|
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:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -272,6 +280,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.16"
|
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:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -349,6 +365,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.12.1"
|
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:
|
stream_channel:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -381,6 +405,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.4"
|
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:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ dependencies:
|
|||||||
provider: ^6.1.1 # 状态管理
|
provider: ^6.1.1 # 状态管理
|
||||||
shared_preferences: ^2.2.2 # 本地存储
|
shared_preferences: ^2.2.2 # 本地存储
|
||||||
awesome_dialog: ^3.3.0
|
awesome_dialog: ^3.3.0
|
||||||
|
toggle_switch: ^2.3.0
|
||||||
|
flutter_riverpod: ^2.4.9
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user