From 8f6e4cc23ee4ca2fb29d458214967785d6da7c4f Mon Sep 17 00:00:00 2001 From: Cxx0822 <1556464090@qq.com> Date: Sun, 26 Apr 2026 14:23:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=A2=9E=E5=8A=A0=E6=97=A5=E7=A8=8B?= =?UTF-8?q?=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/main.dart | 16 +-- lib/models/Schedule.dart | 18 +++ lib/models/task.dart | 18 +-- lib/pages/schedule_page.dart | 204 +++++++++++++++++++++++------- lib/pages/task_page.dart | 38 ++---- lib/widges/task_dialog.dart | 211 ------------------------------- lib/widges/task_item.dart | 151 ---------------------- lib/widgets/common.dart | 106 ++++++++++++++++ lib/widgets/schedule_dialog.dart | 175 +++++++++++++++++++++++++ lib/widgets/task_dialog.dart | 176 ++++++++++++++++++++++++++ lib/widgets/task_item.dart | 188 +++++++++++++++++++++++++++ pubspec.lock | 58 ++++++++- pubspec.yaml | 64 +--------- 13 files changed, 905 insertions(+), 518 deletions(-) create mode 100644 lib/models/Schedule.dart delete mode 100644 lib/widges/task_dialog.dart delete mode 100644 lib/widges/task_item.dart create mode 100644 lib/widgets/common.dart create mode 100644 lib/widgets/schedule_dialog.dart create mode 100644 lib/widgets/task_dialog.dart create mode 100644 lib/widgets/task_item.dart diff --git a/lib/main.dart b/lib/main.dart index e71b89e..e83819e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,7 +14,7 @@ class TaskHubApp extends StatelessWidget { @override Widget build(BuildContext context) { return FluentApp( - title: 'TaskHub - 待办清单', + title: 'TaskHub', themeMode: ThemeMode.system, theme: FluentThemeData( fontFamily: 'CustomFont', @@ -38,22 +38,16 @@ class _MainLayoutState extends State { @override Widget build(BuildContext context) { return NavigationView( + titleBar: TitleBar( + icon: const FlutterLogo(), + title: const Text('TaskHub') + ), pane: NavigationPane( selected: _currentIndex, onChanged: (index) => setState(() => _currentIndex = index), size: NavigationPaneSize( openWidth: 150 ), - header: const Padding( - padding: EdgeInsets.all(10), - child: Text( - 'TaskHub', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - ), items: [ PaneItem( icon: const Icon(FluentIcons.home), diff --git a/lib/models/Schedule.dart b/lib/models/Schedule.dart new file mode 100644 index 0000000..817e991 --- /dev/null +++ b/lib/models/Schedule.dart @@ -0,0 +1,18 @@ +class Schedule { + final int id; + String title; + DateTime startTime; + DateTime endTime; + DateTime? scheduleDate; + DateTime createTime; + + Schedule({ + int? id, + required this.title, + required this.startTime, + required this.endTime, + this.scheduleDate, + DateTime? createTime, + }) : id = id ?? DateTime.timestamp().microsecond, + createTime = DateTime.now(); +} \ No newline at end of file diff --git a/lib/models/task.dart b/lib/models/task.dart index fb568e8..fd44320 100644 --- a/lib/models/task.dart +++ b/lib/models/task.dart @@ -1,24 +1,26 @@ class Task { - final String id; + final int id; String title; - String? description; - DateTime createdAt; + String? desc; DateTime? dueDate; + DateTime? scheduleDate; bool isCompleted; Priority priority; String? category; + DateTime createTime; Task({ - String? id, + int? id, required this.title, - this.description, - DateTime? createdAt, + this.desc, this.dueDate, + this.scheduleDate, this.isCompleted = false, this.priority = Priority.medium, this.category, - }) : id = id ?? DateTime.timestamp().microsecond.toString(), - createdAt = createdAt ?? DateTime.now(); + DateTime? createTime, + }) : id = id ?? DateTime.timestamp().microsecond, + createTime = DateTime.now(); } enum Priority { low, medium, high } \ No newline at end of file diff --git a/lib/pages/schedule_page.dart b/lib/pages/schedule_page.dart index dd261db..6cec940 100644 --- a/lib/pages/schedule_page.dart +++ b/lib/pages/schedule_page.dart @@ -1,6 +1,8 @@ import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/material.dart' show ButtonSegment, SegmentedButton; import 'package:syncfusion_flutter_calendar/calendar.dart' as calendar; +import 'package:task_hub/models/Schedule.dart'; +import 'package:task_hub/widgets/schedule_dialog.dart'; class AppointmentDataSource extends calendar.CalendarDataSource { AppointmentDataSource(List source) { @@ -16,14 +18,106 @@ class SchedulePage extends StatefulWidget { } class _SchedulePageState extends State { - final calendar.CalendarController _calendarController = calendar.CalendarController(); + final calendar.CalendarController _calendarController = + calendar.CalendarController(); late List _appointments = []; String _viewType = 'week'; + void _addSchedule() { + showDialog( + context: context, + builder: + (context) => ScheduleDialog( + onSave: (schedule) { + setState(() { + final appointment = calendar.Appointment( + id: schedule.id, + subject: schedule.title, + startTime: schedule.startTime, + endTime: schedule.endTime, + ); + _appointments.add(appointment); + }); + }, + ), + ); + } + + void _editSchedule(calendar.Appointment appointment) { + final schedule = Schedule( + id: appointment.id as int, + title: appointment.subject, + startTime: appointment.startTime, + endTime: appointment.endTime, + ); + + showDialog( + context: context, + builder: + (context) => ScheduleDialog( + schedule: schedule, + onSave: (editedSchedule) { + setState(() { + final index = _appointments.indexWhere( + (t) => t.id == editedSchedule.id, + ); + if (index != -1) { + final appointment = calendar.Appointment( + id: editedSchedule.id, + subject: editedSchedule.title, + startTime: editedSchedule.startTime, + endTime: editedSchedule.endTime, + ); + _appointments[index] = appointment; + } + }); + }, + ), + ); + } + + void _deleteSchedule(calendar.Appointment appointment) async { + await showDialog( + context: context, + builder: + (context) => ContentDialog( + title: const Text('确认删除该日程?'), + actions: [ + Button( + child: const Text('取消'), + onPressed: () => Navigator.pop(context), + ), + FilledButton( + child: const Text('确认'), + onPressed: () { + setState(() { + _appointments.removeWhere((schedule) => schedule.id == appointment.id); + }); + Navigator.pop(context); + // Delete file here + }, + ) + ], + ), + ); + } + @override Widget build(BuildContext context) { return ScaffoldPage.withPadding( - header: const PageHeader(title: Text('日程安排')), + header: PageHeader( + title: const Text('日程安排'), + commandBar: CommandBar( + mainAxisAlignment: MainAxisAlignment.end, + primaryItems: [ + CommandBarButton( + icon: const Icon(FluentIcons.add), + label: const Text('添加日程'), + onPressed: _addSchedule, + ), + ], + ), + ), content: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -34,7 +128,7 @@ class _SchedulePageState extends State { onSelectionChanged: (Set value) { setState(() { _viewType = value.first; - switch(_viewType) { + switch (_viewType) { case 'week': _calendarController.view = calendar.CalendarView.week; break; @@ -50,7 +144,10 @@ class _SchedulePageState extends State { segments: const [ ButtonSegment( value: 'week', - label: Text('周视图', style: TextStyle(fontFamily: 'CustomFont')), + label: Text( + '周视图', + style: TextStyle(fontFamily: 'CustomFont'), + ), ), ButtonSegment( value: 'month', @@ -69,49 +166,68 @@ class _SchedulePageState extends State { ], ), const SizedBox(height: 20), - Expanded( - child: calendar.SfCalendar( - view: calendar.CalendarView.week, - controller: _calendarController, - backgroundColor: Colors.grey[20], - headerStyle: calendar.CalendarHeaderStyle( - backgroundColor: Colors.grey[20], - ), - dataSource: AppointmentDataSource(_appointments), - firstDayOfWeek: 1, - showDatePickerButton: true, - showNavigationArrow: true, - showTodayButton: true, - allowViewNavigation: true, - // 月份视图设置 - monthViewSettings: calendar.MonthViewSettings( - appointmentDisplayMode: - calendar.MonthAppointmentDisplayMode.appointment, - showAgenda: true, - ), - scheduleViewSettings: calendar.ScheduleViewSettings( - monthHeaderSettings: calendar.MonthHeaderSettings( - backgroundColor: Colors.grey[20], - height: 85, - ), - ), - // 时间区域设置 - timeSlotViewSettings: calendar.TimeSlotViewSettings( - startHour: 7, - endHour: 23, - timeFormat: 'HH:mm', - timeInterval: Duration(minutes: 30), - timeRulerSize: 60, - ), - // 选择日期回调 - onTap: (calendar.CalendarTapDetails details) {}, - // 选择日程回调 - onLongPress: (calendar.CalendarLongPressDetails details) async {}, - ), - ), + Expanded(child: _buildSfCalendar()), ], ), ), ); } + + Widget _buildSfCalendar() { + return calendar.SfCalendar( + view: calendar.CalendarView.week, + controller: _calendarController, + backgroundColor: Colors.grey[20], + headerStyle: calendar.CalendarHeaderStyle( + backgroundColor: Colors.grey[20], + ), + dataSource: AppointmentDataSource(_appointments), + firstDayOfWeek: 1, + showDatePickerButton: true, + showNavigationArrow: true, + showTodayButton: true, + allowViewNavigation: true, + // 月份视图设置 + monthViewSettings: calendar.MonthViewSettings( + appointmentDisplayMode: + calendar.MonthAppointmentDisplayMode.appointment, + showAgenda: true, + ), + scheduleViewSettings: calendar.ScheduleViewSettings( + monthHeaderSettings: calendar.MonthHeaderSettings( + backgroundColor: Colors.grey[20], + height: 85, + ), + ), + // 时间区域设置 + timeSlotViewSettings: calendar.TimeSlotViewSettings( + startHour: 7, + endHour: 23, + timeFormat: 'HH:mm', + timeInterval: Duration(minutes: 30), + timeRulerSize: 60, + ), + // 选择日期回调 + onTap: (calendar.CalendarTapDetails details) { + if (details.targetElement == calendar.CalendarElement.calendarCell) { + if (!details.date!.isBefore(DateTime.now())) { + _addSchedule(); + } + } else if (details.targetElement == + calendar.CalendarElement.appointment) { + if (details.appointments?.length == 1) { + _editSchedule(details.appointments!.first); + } + } + }, + // 选择日程回调 + onLongPress: (calendar.CalendarLongPressDetails details) async { + if (details.targetElement == calendar.CalendarElement.appointment) { + if (details.appointments?.length == 1) { + _deleteSchedule(details.appointments!.first); + } + } + }, + ); + } } diff --git a/lib/pages/task_page.dart b/lib/pages/task_page.dart index e150a83..3679e00 100644 --- a/lib/pages/task_page.dart +++ b/lib/pages/task_page.dart @@ -3,8 +3,8 @@ import 'dart:ui'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/material.dart' show SegmentedButton, ButtonSegment; import 'package:task_hub/models/task.dart'; -import 'package:task_hub/widges/task_dialog.dart'; -import 'package:task_hub/widges/task_item.dart'; +import 'package:task_hub/widgets/task_dialog.dart'; +import 'package:task_hub/widgets/task_item.dart'; // 任务页面 class TaskPage extends StatefulWidget { @@ -16,8 +16,8 @@ class TaskPage extends StatefulWidget { class _TaskPageState extends State { final List _tasks = []; - String _filter = 'all'; // all, active, completed - String _sortBy = 'created'; // created, due, priority + String _filter = 'all'; + String _sortBy = 'created'; List get _filteredTasks { List tasks = @@ -38,7 +38,7 @@ class _TaskPageState extends State { return b.priority.index.compareTo(a.priority.index); case 'created': default: - return b.createdAt.compareTo(a.createdAt); + return b.createTime.compareTo(a.createTime); } }); @@ -77,7 +77,7 @@ class _TaskPageState extends State { ); } - void _deleteTask(String taskId) { + void _deleteTask(int taskId) { setState(() { _tasks.removeWhere((task) => task.id == taskId); }); @@ -90,8 +90,8 @@ class _TaskPageState extends State { _tasks[index] = Task( id: task.id, title: task.title, - description: task.description, - createdAt: task.createdAt, + desc: task.desc, + createTime: task.createTime, dueDate: task.dueDate, isCompleted: !task.isCompleted, priority: task.priority, @@ -101,24 +101,6 @@ class _TaskPageState extends State { }); } - void _updateTaskPriority(String taskId, Priority priority) { - setState(() { - final index = _tasks.indexWhere((task) => task.id == taskId); - if (index != -1) { - _tasks[index] = Task( - id: _tasks[index].id, - title: _tasks[index].title, - description: _tasks[index].description, - createdAt: _tasks[index].createdAt, - dueDate: _tasks[index].dueDate, - isCompleted: _tasks[index].isCompleted, - priority: priority, - category: _tasks[index].category, - ); - } - }); - } - @override Widget build(BuildContext context) { return ScaffoldPage.withPadding( @@ -274,9 +256,7 @@ class _TaskPageState extends State { task: task, onToggleComplete: () => _toggleTaskComplete(task), onEdit: () => _editTask(task), - onDelete: () => _deleteTask(task.id), - onPriorityChanged: - (priority) => _updateTaskPriority(task.id, priority), + onDelete: () => _deleteTask(task.id) ), ); }, diff --git a/lib/widges/task_dialog.dart b/lib/widges/task_dialog.dart deleted file mode 100644 index fcc5fc2..0000000 --- a/lib/widges/task_dialog.dart +++ /dev/null @@ -1,211 +0,0 @@ -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:task_hub/models/task.dart'; - -class TaskDialog extends StatefulWidget { - final Task? task; - final ValueChanged onSave; - - const TaskDialog({super.key, this.task, required this.onSave}); - - @override - State createState() => _TaskDialogState(); -} - -class _TaskDialogState extends State { - late TextEditingController _titleController; - late TextEditingController _descriptionController; - late Priority _selectedPriority; - DateTime? _selectedDate; - final _formKey = GlobalKey(); - - @override - void initState() { - super.initState(); - _titleController = TextEditingController(text: widget.task?.title ?? ''); - _descriptionController = TextEditingController( - text: widget.task?.description ?? '', - ); - _selectedPriority = widget.task?.priority ?? Priority.medium; - _selectedDate = widget.task?.dueDate; - } - - @override - void dispose() { - _titleController.dispose(); - _descriptionController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return ContentDialog( - title: Text(widget.task == null ? '添加任务' : '编辑任务'), - content: Form( - key: _formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // 标题 - _buildFormRow( - label: '任务标题', - required: true, - child: TextFormBox( - controller: _titleController, - placeholder: '请输入任务标题', - validator: (value) { - if (value == null || value.isEmpty) { - return '任务标题不能为空'; - } - return null; - }, - ), - ), - const SizedBox(height: 16), - - // 描述 - _buildFormRow( - label: '任务描述', - child: TextFormBox( - controller: _descriptionController, - placeholder: '请输入任务描述', - maxLines: 3, - ), - ), - const SizedBox(height: 16), - - // 优先级 - _buildFormRow( - label: '优先级', - child: RadioGroup( - groupValue: _selectedPriority, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - RadioButton( - value: Priority.low, - content: Text('低'), - ), - RadioButton( - value: Priority.medium, - content: Text('中'), - ), - RadioButton( - value: Priority.high, - content: Text('高'), - ) - ], - ), - onChanged: (value) { - if (value != null) { - setState(() { - _selectedPriority = value; - }); - } - }, - ), - ), - const SizedBox(height: 16), - - // 截止日期 - _buildFormRow( - label: '截止日期', - child: DatePicker( - selected: _selectedDate, - onChanged: (date) { - setState(() { - _selectedDate = date; - }); - }, - ), - ), - ], - ), - ), - actions: [ - Button( - child: const Text('取消'), - onPressed: () => Navigator.pop(context), - ), - FilledButton( - child: const Text('保存'), - onPressed: () { - if (_formKey.currentState!.validate()) { - final task = Task( - id: widget.task?.id, - title: _titleController.text, - description: _descriptionController.text.isEmpty - ? null - : _descriptionController.text, - dueDate: _selectedDate, - priority: _selectedPriority, - isCompleted: widget.task?.isCompleted ?? false, - ); - widget.onSave(task); - Navigator.pop(context); - } - }, - ), - ], - ); - } - - // 构建表单行 - Widget _buildFormRow({ - required String label, - required Widget child, - bool required = false, - }) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 80, - child: Padding( - padding: const EdgeInsets.only(top: 8), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (required) - Padding( - padding: EdgeInsets.only(right: 4), - child: Text( - '*', - style: TextStyle(color: Colors.red, fontSize: 16), - ), - ), - Text( - label, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - const Text( - ':', - style: TextStyle(fontSize: 14), - ), - ], - ), - ), - ), - const SizedBox(width: 12), - // 右侧表单控件 - Expanded( - child: child, - ), - ], - ); - } - - String _getPriorityLabel(Priority priority) { - switch (priority) { - case Priority.low: - return '低'; - case Priority.medium: - return '中'; - case Priority.high: - return '高'; - } - } -} \ No newline at end of file diff --git a/lib/widges/task_item.dart b/lib/widges/task_item.dart deleted file mode 100644 index 112f200..0000000 --- a/lib/widges/task_item.dart +++ /dev/null @@ -1,151 +0,0 @@ -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:task_hub/models/task.dart'; - -class TaskItem extends StatelessWidget { - final Task task; - final VoidCallback onToggleComplete; - final VoidCallback onEdit; - final VoidCallback onDelete; - final ValueChanged onPriorityChanged; - - const TaskItem({ - super.key, - required this.task, - required this.onToggleComplete, - required this.onEdit, - required this.onDelete, - required this.onPriorityChanged, - }); - - @override - Widget build(BuildContext context) { - return Card( - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Row( - children: [ - // 完成状态复选框 - Checkbox( - checked: task.isCompleted, - onChanged: (value) => onToggleComplete(), - ), - const SizedBox(width: 12), - // 任务信息 - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - // 优先级指示器 - _buildPriorityIndicator(task.priority), - const SizedBox(width: 8), - Expanded( - child: Text( - task.title, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - decoration: - task.isCompleted - ? TextDecoration.lineThrough - : null, - color: task.isCompleted ? Colors.grey : Colors.red, - ), - ), - ), - ], - ), - const SizedBox(height: 2), - if (task.description != null && task.description!.isNotEmpty) - Padding( - padding: const EdgeInsets.only(top: 4.0), - child: Text( - task.description!, - style: TextStyle( - color: Colors.grey, - fontSize: 14, - decoration: - task.isCompleted - ? TextDecoration.lineThrough - : null, - ), - ), - ), - const SizedBox(height: 2), - if (task.dueDate != null) - Padding( - padding: const EdgeInsets.only(top: 4.0), - child: Row( - children: [ - const Icon( - FluentIcons.calendar, - size: 12, - color: Colors.grey, - ), - const SizedBox(width: 4), - Text( - '截止: ${_formatDate(task.dueDate!)}', - style: const TextStyle( - color: Colors.grey, - fontSize: 12, - ), - ), - ], - ), - ), - ], - ), - ), - // 操作按钮 - Row( - children: [ - // 编辑按钮 - IconButton( - icon: const Icon(FluentIcons.edit, size: 16), - onPressed: onEdit, - ), - // 删除按钮 - IconButton( - icon: const Icon(FluentIcons.delete, size: 16), - onPressed: onDelete, - ), - ], - ), - ], - ), - ), - ); - } - - Widget _buildPriorityIndicator(Priority priority) { - final Map priorityInfo = { - Priority.low: (Colors.grey, '低'), - Priority.medium: (Colors.green, '中'), - Priority.high: (Colors.red, '高'), - }; - - final (color, label) = priorityInfo[priority]!; - return Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3), - decoration: BoxDecoration( - color: color, - borderRadius: BorderRadius.circular(4), - ), - child: Center( - child: Text( - label, - style: const TextStyle( - color: Colors.white, - fontSize: 10, - fontWeight: FontWeight.bold, - ), - ), - ), - ); - } - - String _formatDate(DateTime date) { - return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; - } -} \ No newline at end of file diff --git a/lib/widgets/common.dart b/lib/widgets/common.dart new file mode 100644 index 0000000..d7c5d1a --- /dev/null +++ b/lib/widgets/common.dart @@ -0,0 +1,106 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:intl/intl.dart'; +import 'package:omni_datetime_picker/omni_datetime_picker.dart'; + +Widget buildFormItem({ + required String label, + required Widget child, + bool required = false, +}) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 80, + child: Padding( + padding: const EdgeInsets.only(top: 8), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (required) + Padding( + padding: EdgeInsets.only(right: 4), + child: Text( + '*', + style: TextStyle(color: Colors.red, fontSize: 16), + ), + ), + Text( + label, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + const Text(':', style: TextStyle(fontSize: 14)), + ], + ), + ), + ), + const SizedBox(width: 12), + // 右侧表单控件 + Expanded(child: child), + ], + ); +} + +// 显示日期/日期时间的按钮组件 +Widget buildOmniDateButton({ + required BuildContext context, + required DateTime? selectedDate, + required String placeholder, + required OmniDateTimePickerType type, + required ValueChanged onChanged, +}) { + String displayText = '$placeholder'; + + if (selectedDate != null) { + switch (type) { + case OmniDateTimePickerType.date: + displayText = DateFormat('yyyy-MM-dd').format(selectedDate); + break; + case OmniDateTimePickerType.time: + displayText = DateFormat('HH:mm:ss').format(selectedDate); + break; + case OmniDateTimePickerType.dateAndTime: + displayText = DateFormat('yyyy-MM-dd HH:mm:ss').format(selectedDate); + break; + } + } + + return Button( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(FluentIcons.calendar, size: 16), + SizedBox(width: 8), + Text( + displayText, + style: TextStyle( + color: selectedDate == null ? Colors.grey[130] : null, + ), + ), + ], + ), + onPressed: () async { + final DateTime? picked = await showOmniDateTimePicker( + context: context, + initialDate: selectedDate ?? DateTime.now(), + firstDate: DateTime( + DateTime.now().year, + DateTime.now().month, + DateTime.now().day, + ), + lastDate: DateTime.now().add(Duration(days: 365 * 2)), + is24HourMode: true, + isShowSeconds: false, + type: type, + barrierColor: Colors.grey, + ); + + if (picked != null) { + onChanged(picked); + } + }, + ); +} diff --git a/lib/widgets/schedule_dialog.dart b/lib/widgets/schedule_dialog.dart new file mode 100644 index 0000000..c48e982 --- /dev/null +++ b/lib/widgets/schedule_dialog.dart @@ -0,0 +1,175 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:omni_datetime_picker/omni_datetime_picker.dart'; +import 'package:task_hub/models/Schedule.dart'; +import 'package:task_hub/widgets/common.dart'; + +class ScheduleDialog extends StatefulWidget { + final Schedule? schedule; + final ValueChanged onSave; + + const ScheduleDialog({super.key, this.schedule, required this.onSave}); + + @override + State createState() => _ScheduleDialogState(); +} + +class _ScheduleDialogState extends State { + late TextEditingController _titleController; + DateTime? _selectedStartTime; + DateTime? _selectedEndTime; + DateTime? _selectedScheduleDate; + final _formKey = GlobalKey(); + + @override + void initState() { + super.initState(); + _titleController = TextEditingController(text: widget.schedule?.title ?? ''); + _selectedStartTime = widget.schedule?.startTime; + _selectedEndTime = widget.schedule?.endTime; + _selectedScheduleDate = widget.schedule?.scheduleDate; + } + + @override + void dispose() { + _titleController.dispose(); + super.dispose(); + } + + void onConfirmClick()async { + if (_selectedStartTime == null) { + await displayInfoBar(context, builder: (context, close) { + return InfoBar( + title: const Text('请选择开始时间'), + severity: InfoBarSeverity.error, + ); + }); + return; + } + + if (_selectedEndTime == null) { + await displayInfoBar(context, builder: (context, close) { + return InfoBar( + title: const Text('请选择结束时间'), + severity: InfoBarSeverity.error, + ); + }); + return; + } + + if (!_selectedEndTime!.isAfter(_selectedStartTime!)) { + await displayInfoBar(context, builder: (context, close) { + return InfoBar( + title: const Text('结束时间必须晚于开始时间'), + severity: InfoBarSeverity.error, + ); + }); + return; + } + + if (_formKey.currentState!.validate()) { + final schedule = Schedule( + id: widget.schedule?.id, + title: _titleController.text, + startTime: _selectedStartTime!, + endTime: _selectedEndTime!, + scheduleDate: _selectedScheduleDate, + ); + widget.onSave(schedule); + Navigator.pop(context); + } + } + + @override + Widget build(BuildContext context) { + return ContentDialog( + title: Text(widget.schedule == null ? '添加日程' : '编辑日程'), + content: _buildForm(), + actions: [ + Button( + child: const Text('取消'), + onPressed: () => Navigator.pop(context), + ), + FilledButton(child: const Text('保存'), onPressed: onConfirmClick), + ], + ); + } + + Widget _buildForm() { + return Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 标题 + buildFormItem( + label: '日程标题', + required: true, + child: TextFormBox( + controller: _titleController, + placeholder: '请输入日程标题', + validator: (value) { + if (value == null || value.isEmpty) { + return '任务标题不能为空'; + } + return null; + }, + ), + ), + const SizedBox(height: 16), + + // 开始时间 + buildFormItem( + label: '开始时间', + required: true, + child: buildOmniDateButton( + context: context, + selectedDate: _selectedStartTime, + placeholder: '请选择开始时间', + type: OmniDateTimePickerType.dateAndTime, + onChanged: (date) { + setState(() { + _selectedStartTime = date!; + }); + }, + ), + ), + const SizedBox(height: 16), + + // 结束时间 + buildFormItem( + label: '结束时间', + required: true, + child: buildOmniDateButton( + context: context, + selectedDate: _selectedEndTime, + placeholder: '请选择结束时间', + type: OmniDateTimePickerType.dateAndTime, + onChanged: (date) { + setState(() { + _selectedEndTime = date!; + }); + }, + ), + ), + const SizedBox(height: 16), + + // 提醒日期 + buildFormItem( + label: '提醒日期', + child: buildOmniDateButton( + context: context, + selectedDate: _selectedScheduleDate, + placeholder: '请选择提醒日期', + type: OmniDateTimePickerType.dateAndTime, + onChanged: (date) { + setState(() { + _selectedScheduleDate = date; + }); + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/task_dialog.dart b/lib/widgets/task_dialog.dart new file mode 100644 index 0000000..4df39de --- /dev/null +++ b/lib/widgets/task_dialog.dart @@ -0,0 +1,176 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:omni_datetime_picker/omni_datetime_picker.dart'; +import 'package:task_hub/models/task.dart'; +import 'package:task_hub/widgets/common.dart'; + +class TaskDialog extends StatefulWidget { + final Task? task; + final ValueChanged onSave; + + const TaskDialog({super.key, this.task, required this.onSave}); + + @override + State createState() => _TaskDialogState(); +} + +class _TaskDialogState extends State { + late TextEditingController _titleController; + late TextEditingController _descController; + late Priority _selectedPriority; + DateTime? _selectedDueDate; + DateTime? _selectedScheduleDate; + final _formKey = GlobalKey(); + + @override + void initState() { + super.initState(); + _titleController = TextEditingController(text: widget.task?.title ?? ''); + _descController = TextEditingController(text: widget.task?.desc ?? ''); + _selectedPriority = widget.task?.priority ?? Priority.medium; + _selectedDueDate = widget.task?.dueDate; + _selectedScheduleDate = widget.task?.scheduleDate; + } + + @override + void dispose() { + _titleController.dispose(); + _descController.dispose(); + super.dispose(); + } + + void onConfirmClick() { + if (_formKey.currentState!.validate()) { + final task = Task( + id: widget.task?.id, + title: _titleController.text, + desc: _descController.text.isEmpty ? null : _descController.text, + dueDate: _selectedDueDate, + scheduleDate: _selectedScheduleDate, + priority: _selectedPriority, + isCompleted: widget.task?.isCompleted ?? false, + ); + widget.onSave(task); + Navigator.pop(context); + } + } + + @override + Widget build(BuildContext context) { + return ContentDialog( + title: Text(widget.task == null ? '添加任务' : '编辑任务'), + content: _buildForm(), + actions: [ + Button( + child: const Text('取消'), + onPressed: () => Navigator.pop(context), + ), + FilledButton(child: const Text('保存'), onPressed: onConfirmClick), + ], + ); + } + + Widget _buildForm() { + return Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 标题 + buildFormItem( + label: '任务标题', + required: true, + child: TextFormBox( + controller: _titleController, + placeholder: '请输入任务标题', + validator: (value) { + if (value == null || value.isEmpty) { + return '任务标题不能为空'; + } + return null; + }, + ), + ), + const SizedBox(height: 16), + + // 描述 + buildFormItem( + label: '任务描述', + child: TextFormBox( + controller: _descController, + placeholder: '请输入任务描述', + maxLines: 3, + ), + ), + const SizedBox(height: 16), + + // 优先级 + buildFormItem( + label: '优先级', + child: RadioGroup( + groupValue: _selectedPriority, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + RadioButton( + value: Priority.low, + content: Text('低'), + ), + RadioButton( + value: Priority.medium, + content: Text('中'), + ), + RadioButton( + value: Priority.high, + content: Text('高'), + ), + ], + ), + onChanged: (value) { + if (value != null) { + setState(() { + _selectedPriority = value; + }); + } + }, + ), + ), + const SizedBox(height: 16), + + // 截止日期 + buildFormItem( + label: '截止日期', + child: buildOmniDateButton( + context: context, + selectedDate: _selectedDueDate, + placeholder: '请选择截止日期', + type: OmniDateTimePickerType.date, + onChanged: (date) { + setState(() { + _selectedDueDate = date; + }); + }, + ), + ), + const SizedBox(height: 16), + + // 提醒日期 + buildFormItem( + label: '提醒日期', + child: buildOmniDateButton( + context: context, + selectedDate: _selectedScheduleDate, + placeholder: '请选择提醒日期', + type: OmniDateTimePickerType.dateAndTime, + onChanged: (date) { + setState(() { + _selectedScheduleDate = date; + }); + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/task_item.dart b/lib/widgets/task_item.dart new file mode 100644 index 0000000..acb8ee6 --- /dev/null +++ b/lib/widgets/task_item.dart @@ -0,0 +1,188 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:task_hub/models/task.dart'; + +class TaskItem extends StatelessWidget { + final Task task; + final VoidCallback onToggleComplete; + final VoidCallback onEdit; + final VoidCallback onDelete; + + const TaskItem({ + super.key, + required this.task, + required this.onToggleComplete, + required this.onEdit, + required this.onDelete, + }); + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + children: [ + Checkbox( + checked: task.isCompleted, + onChanged: (value) => onToggleComplete(), + ), + const SizedBox(width: 12), + // 任务信息 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + _buildPriorityIndicator(task.priority), + const SizedBox(width: 8), + Expanded(child: _buildTitle()), + ], + ), + const SizedBox(height: 2), + if (task.desc != null && task.desc!.isNotEmpty) _buildDesc(), + const SizedBox(height: 2), + if (task.dueDate != null || task.scheduleDate != null) + _buildDate(), + ], + ), + ), + Row( + children: [ + IconButton( + icon: const Icon(FluentIcons.edit, size: 16), + onPressed: onEdit, + ), + _buildDeleteButton(), + ], + ), + ], + ), + ), + ); + } + + Widget _buildTitle() { + return Text( + task.title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + decoration: task.isCompleted ? TextDecoration.lineThrough : null, + color: task.isCompleted ? Colors.grey : Colors.red, + ), + ); + } + + Widget _buildDesc() { + return Text( + task.desc!, + style: TextStyle( + color: Colors.grey, + fontSize: 14, + decoration: task.isCompleted ? TextDecoration.lineThrough : null, + ), + ); + } + + Widget _buildDate() { + return Row( + children: [ + if (task.dueDate != null) ...[ + const Icon(FluentIcons.calendar, size: 12, color: Colors.grey), + const SizedBox(width: 4), + Text( + '截止日期: ${_formatDate(task.dueDate!)}', + style: const TextStyle(color: Colors.grey, fontSize: 12), + ), + const SizedBox(width: 4), + ], + if (task.scheduleDate != null) ...[ + const Icon(FluentIcons.clock, size: 12, color: Colors.grey), + const SizedBox(width: 4), + Text( + '提醒日期: ${_formatDate(task.scheduleDate!)}', + style: const TextStyle(color: Colors.grey, fontSize: 12), + ), + ], + ], + ); + } + + Widget _buildPriorityIndicator(Priority priority) { + final Map priorityInfo = { + Priority.low: (Colors.grey, '低'), + Priority.medium: (Colors.green, '中'), + Priority.high: (Colors.red, '高'), + }; + + final (color, label) = priorityInfo[priority]!; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3), + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(4), + ), + child: Center( + child: Text( + label, + style: const TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + ), + ), + ); + } + + Widget _buildDeleteButton() { + final controller = FlyoutController(); + + return FlyoutTarget( + controller: controller, + child: IconButton( + icon: const Icon(FluentIcons.delete, size: 16), + onPressed: () { + controller.showFlyout( + builder: (context) { + return FlyoutContent( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('确认删除该任务?'), + const SizedBox(height: 12.0), + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Button( + onPressed: () { + onDelete(); + Flyout.of(context).close(); + }, + child: const Text('确认'), + ), + const SizedBox(width: 5), + Button( + onPressed: Flyout.of(context).close, + child: const Text('取消'), + ), + ], + ), + ], + ), + ); + }, + ); + }, + ), + ); + } + + String _formatDate(DateTime date) { + return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; + } +} diff --git a/pubspec.lock b/pubspec.lock index 2dc73f1..fa108c2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.12.0" + bloc: + dependency: transitive + description: + name: bloc + sha256: a48653a82055a900b88cd35f92429f068c5a8057ae9b136d197b3d56c57efb81 + url: "https://pub.flutter-io.cn" + source: hosted + version: "9.2.0" boolean_selector: dependency: transitive description: @@ -65,6 +73,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.7.12" + equatable: + dependency: transitive + description: + name: equatable + sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.8" fake_async: dependency: transitive description: @@ -94,6 +110,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_bloc: + dependency: transitive + description: + name: flutter_bloc + sha256: cf51747952201a455a1c840f8171d273be009b932c75093020f9af64f2123e38 + url: "https://pub.flutter-io.cn" + source: hosted + version: "9.1.1" flutter_lints: dependency: "direct dev" description: @@ -161,7 +185,7 @@ packages: source: hosted version: "4.1.2" intl: - dependency: transitive + dependency: "direct main" description: name: intl sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" @@ -232,6 +256,22 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.17.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.0" + omni_datetime_picker: + dependency: "direct main" + description: + name: omni_datetime_picker + sha256: bb360790e76109ea2e53b45643cdaab779649c4bf1a9d2794d3a135bfe9746e1 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.1" path: dependency: transitive description: @@ -256,6 +296,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.1.8" + provider: + dependency: transitive + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.1.5+1" recase: dependency: transitive description: @@ -264,6 +312,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "4.1.0" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.28.0" scroll_pos: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 6ce08d1..907fcc8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,63 +1 @@ -name: task_hub -description: "A new Flutter project." -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev - -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -# In Windows, build-name is used as the major, minor, and patch parts -# of the product and file versions while build-number is used as the build suffix. -version: 1.0.0+1 - -environment: - sdk: ^3.7.0 - -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. -dependencies: - flutter: - sdk: flutter - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.8 - flutter_local_notifications: ^19.5.0 - syncfusion_flutter_calendar: ^30.1.37 - fluent_ui: ^4.15.0 - -dev_dependencies: - flutter_test: - sdk: flutter - - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. - flutter_lints: ^5.0.0 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. -flutter: - uses-material-design: true - assets: - - assets/icons/task.png - fonts: - - family: CustomFont - fonts: - - asset: fonts/custom.ttf +name: task_hub description: "A new Flutter project." # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 # followed by an optional build number separated by a +. # Both the version and the builder number may be overridden in flutter # build by specifying --build-name and --build-number, respectively. # In Android, build-name is used as versionName while build-number used as versionCode. # Read more about Android versioning at https://developer.android.com/studio/publish/versioning # In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. version: 1.0.0+1 environment: sdk: ^3.7.0 # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions # consider running `flutter pub upgrade --major-versions`. Alternatively, # dependencies can be manually updated by changing the version numbers below to # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 flutter_local_notifications: ^19.5.0 syncfusion_flutter_calendar: ^30.1.37 fluent_ui: ^4.15.0 omni_datetime_picker: ^2.3.1 intl: ^0.20.2 dev_dependencies: flutter_test: sdk: flutter # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. flutter_lints: ^5.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter packages. flutter: uses-material-design: true assets: - assets/icons/task.png fonts: - family: CustomFont fonts: - asset: fonts/custom.ttf \ No newline at end of file