feat:更新待办实现对话框内容

This commit is contained in:
2025-11-06 20:02:59 +08:00
parent d76c01ba9d
commit 6c3d2c9099
9 changed files with 407 additions and 120 deletions

51
lib/models/common.dart Normal file
View File

@@ -0,0 +1,51 @@
import 'package:awesome_dialog/awesome_dialog.dart';
import 'package:flutter/material.dart';
BoxDecoration buildBoxDecoration() {
return BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[200]!, width: 1),
);
}
Container buildBody({Widget? child}) {
return Container(
color: Colors.grey[50],
padding: EdgeInsets.all(5),
child: SingleChildScrollView(child: child),
);
}
Container buildCard({Widget? child}) {
return Container(
decoration: buildBoxDecoration(),
padding: EdgeInsets.all(10),
child: child,
);
}
void showAwesomeDialog({
required BuildContext context,
required Widget body,
required VoidCallback onOk,
required VoidCallback onCancel,
}) {
AwesomeDialog(
context: context,
dialogType: DialogType.noHeader,
animType: AnimType.scale,
body: body,
dialogBackgroundColor: Colors.white,
btnOkText: "确认",
btnCancelText: "取消",
btnOkColor: Colors.orange,
btnCancelColor: Colors.grey,
buttonsBorderRadius: BorderRadius.circular(10),
headerAnimationLoop: false,
dismissOnTouchOutside: false,
dismissOnBackKeyPress: true,
btnOkOnPress: onOk,
btnCancelOnPress: onCancel,
).show();
}

View File

@@ -1,3 +1,5 @@
import 'package:awesome_dialog/awesome_dialog.dart';
import 'package:flisp_app/models/common.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; // 用于复制功能
@@ -72,6 +74,7 @@ enum TodoFilter {
today('今天');
final String label;
const TodoFilter(this.label);
}
@@ -107,16 +110,24 @@ class _TodoPageState extends State<TodoPage> {
// 统计数据
int get _totalCount => _todos.length;
int get _activeCount => _todos.where((todo) => !todo.isCompleted).length;
int get _completedCount => _todos.where((todo) => todo.isCompleted).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
title: const Text('待办事项'),
backgroundColor: Colors.orange,
foregroundColor: Colors.white
),
body: Column(
children: [
@@ -128,7 +139,9 @@ class _TodoPageState extends State<TodoPage> {
// 待办事项列表
Expanded(
child: _filteredTodos.isEmpty ? _buildEmptyState() : _buildTodoList(),
child: _filteredTodos.isEmpty
? _buildEmptyState()
: _buildTodoList(),
),
],
),
@@ -287,7 +300,8 @@ class _TodoPageState extends State<TodoPage> {
// 构建待办事项副标题
Widget? _buildTodoSubtitle(TodoItem todo, bool isOverdue) {
final hasContent = todo.description?.isNotEmpty == true || todo.dueDate != null;
final hasContent = todo.description?.isNotEmpty == true ||
todo.dueDate != null;
if (!hasContent) return null;
@@ -335,10 +349,11 @@ class _TodoPageState extends State<TodoPage> {
// 显示添加待办事项对话框
void _showAddTodoDialog() {
_resetForm();
showDialog(
context: context,
builder: (context) => _buildTodoDialog(isEditing: false),
);
showAwesomeDialog(context: context,
body: _buildTodoDialogContent(isEditing: true),
onOk: () => _saveTodo(false, null),
onCancel: () => _resetForm());
}
// 显示编辑待办事项对话框
@@ -349,92 +364,208 @@ class _TodoPageState extends State<TodoPage> {
_selectedPriority = todo.priority;
_selectedCategory = todo.category;
showDialog(
context: context,
builder: (context) => _buildTodoDialog(isEditing: true, todo: todo),
);
showAwesomeDialog(context: context,
body: _buildTodoDialogContent(isEditing: true),
onOk: () => _saveTodo(false, null),
onCancel: () => _resetForm());
}
// 构建待办事项对话框
Widget _buildTodoDialog({bool isEditing = false, TodoItem? todo}) {
return AlertDialog(
title: Text(isEditing ? '编辑待办事项' : '添加待办事项'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: _titleController,
decoration: const InputDecoration(
labelText: '标题*',
border: OutlineInputBorder(),
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,
),
maxLength: 100,
),
const SizedBox(height: 12),
TextField(
controller: _descriptionController,
decoration: const InputDecoration(
labelText: '描述',
border: OutlineInputBorder(),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade200),
),
maxLines: 2,
maxLength: 200,
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),
),
const SizedBox(height: 12),
// 截止日期选择
ListTile(
leading: const Icon(Icons.calendar_today),
title: Text(_selectedDueDate == null
? '选择截止日期'
: '截止: ${_formatDate(_selectedDueDate!)}'),
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: const Icon(Icons.clear),
onPressed: () => setState(() => _selectedDueDate = null),
icon: Icon(Icons.clear, size: 18),
onPressed: () {
setState(() => _selectedDueDate = null);
// 重新显示对话框以更新状态
_showAddTodoDialog();
},
)
: null,
onTap: () => _selectDueDate(),
onTap: () => _selectDueDateInDialog(),
),
// 优先级选择
ListTile(
leading: const Icon(Icons.flag),
title: const Text('优先级'),
),
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,
onChanged: (value) => setState(() => _selectedPriority = value!),
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: Row(
children: [
Icon(priority.icon, color: priority.color, size: 16),
const SizedBox(width: 8),
Text(priority.label),
],
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,
),
],
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
ElevatedButton(
onPressed: _validateForm() ? () => _saveTodo(isEditing, todo) : null,
child: Text(isEditing ? '保存' : '添加'),
),
],
);
}
// 选择截止日期
Future<void> _selectDueDate() async {
// 在对话框中选择日期
Future<void> _selectDueDateInDialog() async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: DateTime.now(),
@@ -446,12 +577,16 @@ class _TodoPageState extends State<TodoPage> {
setState(() {
_selectedDueDate = picked;
});
// 重新显示对话框以更新日期显示
_showAddTodoDialog();
}
}
// 验证表单
bool _validateForm() {
return _titleController.text.trim().isNotEmpty;
return _titleController.text
.trim()
.isNotEmpty;
}
// 重置表单
@@ -468,9 +603,14 @@ class _TodoPageState extends State<TodoPage> {
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 ?
description: _descriptionController.text
.trim()
.isEmpty ?
null : _descriptionController.text.trim(),
dueDate: _selectedDueDate,
priority: _selectedPriority,
@@ -481,15 +621,32 @@ class _TodoPageState extends State<TodoPage> {
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);
}
});
Navigator.pop(context);
_resetForm();
// 显示成功提示
_showSuccessDialog(isEditing ? '更新成功' : '添加成功');
}
// 显示成功提示
void _showSuccessDialog(String message) {
AwesomeDialog(
context: context,
dialogType: DialogType.success,
animType: AnimType.scale,
title: message,
btnOkText: "好的",
btnOkColor: Colors.green,
btnOkOnPress: () {},
autoHide: Duration(seconds: 2), // 2秒后自动关闭
).show();
}
// 切换待办事项完成状态
@@ -508,20 +665,21 @@ 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('取消'),
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)),
),
],
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('删除', style: TextStyle(color: Colors.red)),
),
],
),
) ?? false;
}
@@ -540,36 +698,37 @@ class _TodoPageState extends State<TodoPage> {
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);
},
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);
},
),
],
),
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);
},
),
],
),
);
}
@@ -609,7 +768,8 @@ class _TodoPageState extends State<TodoPage> {
// 导出待办事项
void _exportTodos() {
final exportText = _todos.map((todo) {
return '${todo.isCompleted ? '[✓]' : '[ ]'} ${todo.title}${todo.dueDate != null ? ' (截止: ${_formatDate(todo.dueDate!)})' : ''}';
return '${todo.isCompleted ? '[✓]' : '[ ]'} ${todo.title}${todo.dueDate !=
null ? ' (截止: ${_formatDate(todo.dueDate!)})' : ''}';
}).join('\n');
Clipboard.setData(ClipboardData(text: exportText));

View File

@@ -6,6 +6,10 @@
#include "generated_plugin_registrant.h"
#include <rive_native/rive_native_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) rive_native_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "RiveNativePlugin");
rive_native_plugin_register_with_registrar(rive_native_registrar);
}

View File

@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
rive_native
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@@ -5,8 +5,10 @@
import FlutterMacOS
import Foundation
import rive_native
import shared_preferences_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
RiveNativePlugin.register(with: registry.registrar(forPlugin: "RiveNativePlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
}

View File

@@ -1,6 +1,14 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
@@ -9,6 +17,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.12.0"
awesome_dialog:
dependency: "direct main"
description:
name: awesome_dialog
sha256: "4c5821a0a637ceee022084e78c1b8237dd4b8bfca4dd24ac2484662a56707338"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.3.0"
boolean_selector:
dependency: transitive
description:
@@ -96,6 +112,30 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
graphs:
dependency: transitive
description:
name: graphs
sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.3.2"
http:
dependency: transitive
description:
name: http
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.5.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.1.2"
leak_tracker:
dependency: transitive
description:
@@ -216,6 +256,22 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.1.5+1"
rive:
dependency: transitive
description:
name: rive
sha256: "20d91e17b1bae5f4030d252336227f990782c942cb44e41685a13ae5f885e641"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.14.0-dev.13"
rive_native:
dependency: transitive
description:
name: rive_native
sha256: "1b65b473939b008feb30c1f43785996063cefaf4ceddfa1a2ffe6051201e8d3a"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.0.16"
shared_preferences:
dependency: "direct main"
description:
@@ -325,6 +381,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.7.4"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.4.0"
vector_math:
dependency: transitive
description:

View File

@@ -36,6 +36,7 @@ dependencies:
cupertino_icons: ^1.0.8
provider: ^6.1.1 # 状态管理
shared_preferences: ^2.2.2 # 本地存储
awesome_dialog: ^3.3.0
dev_dependencies:
flutter_test:

View File

@@ -6,6 +6,9 @@
#include "generated_plugin_registrant.h"
#include <rive_native/rive_native_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
RiveNativePluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("RiveNativePlugin"));
}

View File

@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
rive_native
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST