feat:增加日程模块

This commit is contained in:
2026-04-26 14:23:32 +08:00
parent 57c6e552d4
commit 8f6e4cc23e
13 changed files with 905 additions and 518 deletions

View File

@@ -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<MainLayout> {
@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),

18
lib/models/Schedule.dart Normal file
View File

@@ -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();
}

View File

@@ -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 }

View File

@@ -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<calendar.Appointment> source) {
@@ -16,14 +18,106 @@ class SchedulePage extends StatefulWidget {
}
class _SchedulePageState extends State<SchedulePage> {
final calendar.CalendarController _calendarController = calendar.CalendarController();
final calendar.CalendarController _calendarController =
calendar.CalendarController();
late List<calendar.Appointment> _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<String>(
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<SchedulePage> {
onSelectionChanged: (Set<String> 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<SchedulePage> {
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<SchedulePage> {
],
),
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);
}
}
},
);
}
}

View File

@@ -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<TaskPage> {
final List<Task> _tasks = [];
String _filter = 'all'; // all, active, completed
String _sortBy = 'created'; // created, due, priority
String _filter = 'all';
String _sortBy = 'created';
List<Task> get _filteredTasks {
List<Task> tasks =
@@ -38,7 +38,7 @@ class _TaskPageState extends State<TaskPage> {
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<TaskPage> {
);
}
void _deleteTask(String taskId) {
void _deleteTask(int taskId) {
setState(() {
_tasks.removeWhere((task) => task.id == taskId);
});
@@ -90,8 +90,8 @@ class _TaskPageState extends State<TaskPage> {
_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<TaskPage> {
});
}
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<TaskPage> {
task: task,
onToggleComplete: () => _toggleTaskComplete(task),
onEdit: () => _editTask(task),
onDelete: () => _deleteTask(task.id),
onPriorityChanged:
(priority) => _updateTaskPriority(task.id, priority),
onDelete: () => _deleteTask(task.id)
),
);
},

View File

@@ -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<Task> onSave;
const TaskDialog({super.key, this.task, required this.onSave});
@override
State<TaskDialog> createState() => _TaskDialogState();
}
class _TaskDialogState extends State<TaskDialog> {
late TextEditingController _titleController;
late TextEditingController _descriptionController;
late Priority _selectedPriority;
DateTime? _selectedDate;
final _formKey = GlobalKey<FormState>();
@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<Priority>(
groupValue: _selectedPriority,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
RadioButton<Priority>(
value: Priority.low,
content: Text(''),
),
RadioButton<Priority>(
value: Priority.medium,
content: Text(''),
),
RadioButton<Priority>(
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 '';
}
}
}

View File

@@ -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<Priority> 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<Priority, (Color, String)> 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')}';
}
}

106
lib/widgets/common.dart Normal file
View File

@@ -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<DateTime?> 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);
}
},
);
}

View File

@@ -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<Schedule> onSave;
const ScheduleDialog({super.key, this.schedule, required this.onSave});
@override
State<ScheduleDialog> createState() => _ScheduleDialogState();
}
class _ScheduleDialogState extends State<ScheduleDialog> {
late TextEditingController _titleController;
DateTime? _selectedStartTime;
DateTime? _selectedEndTime;
DateTime? _selectedScheduleDate;
final _formKey = GlobalKey<FormState>();
@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;
});
},
),
),
],
),
);
}
}

View File

@@ -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<Task> onSave;
const TaskDialog({super.key, this.task, required this.onSave});
@override
State<TaskDialog> createState() => _TaskDialogState();
}
class _TaskDialogState extends State<TaskDialog> {
late TextEditingController _titleController;
late TextEditingController _descController;
late Priority _selectedPriority;
DateTime? _selectedDueDate;
DateTime? _selectedScheduleDate;
final _formKey = GlobalKey<FormState>();
@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<Priority>(
groupValue: _selectedPriority,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
RadioButton<Priority>(
value: Priority.low,
content: Text(''),
),
RadioButton<Priority>(
value: Priority.medium,
content: Text(''),
),
RadioButton<Priority>(
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;
});
},
),
),
],
),
);
}
}

188
lib/widgets/task_item.dart Normal file
View File

@@ -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<Priority, (Color, String)> 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<void>(
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')}';
}
}

View File

@@ -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:

View File

@@ -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