From 54bed08b7ba331d821c0329f060743fdc53cf5c2 Mon Sep 17 00:00:00 2001 From: Cxx0822 <1556464090@qq.com> Date: Thu, 13 Nov 2025 15:53:29 +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/layout/main_screen.dart | 18 +- lib/main.dart | 9 + lib/models/calendar.dart | 54 ++++++ lib/models/calendar.g.dart | 59 ++++++ lib/pages/calendar_page.dart | 175 +++++++++++++++++ lib/pages/todo_page.dart | 4 +- lib/provider/calendar_provider.dart | 25 +++ lib/service/calendar_service.dart | 43 ++++ lib/utils/date_utils.dart | 16 +- lib/utils/notify_utils.dart | 8 +- lib/widgets/calendar_form.dart | 291 ++++++++++++++++++++++++++++ lib/widgets/calendar_widget.dart | 89 +++++++++ lib/widgets/common.dart | 25 ++- pubspec.lock | 32 +++ pubspec.yaml | 2 + 15 files changed, 830 insertions(+), 20 deletions(-) create mode 100644 lib/models/calendar.dart create mode 100644 lib/models/calendar.g.dart create mode 100644 lib/pages/calendar_page.dart create mode 100644 lib/provider/calendar_provider.dart create mode 100644 lib/service/calendar_service.dart create mode 100644 lib/widgets/calendar_form.dart create mode 100644 lib/widgets/calendar_widget.dart diff --git a/lib/layout/main_screen.dart b/lib/layout/main_screen.dart index 01c6f33..6363088 100644 --- a/lib/layout/main_screen.dart +++ b/lib/layout/main_screen.dart @@ -1,4 +1,5 @@ import 'package:flisp_app/layout/app_drawer.dart'; +import 'package:flisp_app/pages/calendar_page.dart'; import 'package:flisp_app/pages/flisp_page.dart'; import 'package:flisp_app/pages/todo_page.dart'; import 'package:flisp_app/provider/app_provider.dart'; @@ -15,10 +16,15 @@ class MainScreen extends StatefulWidget { class _MainScreenState extends State { final GlobalKey _flispPageKey = GlobalKey(); final GlobalKey _todoPageKey = GlobalKey(); + final GlobalKey _calendarPageKey = GlobalKey(); List navItems = [ BottomNavigationBarItem(icon: Icon(Icons.flash_on), label: '闪灵'), BottomNavigationBarItem(icon: Icon(Icons.checklist), label: '待办'), + BottomNavigationBarItem( + icon: Icon(Icons.calendar_month_rounded), + label: '日程', + ), ]; @override @@ -50,7 +56,9 @@ class _MainScreenState extends State { type: BottomNavigationBarType.fixed, backgroundColor: Theme.of(context).colorScheme.surface, selectedItemColor: Theme.of(context).colorScheme.primary, - unselectedItemColor: Theme.of(context).colorScheme.onSurface.withAlpha(120), + unselectedItemColor: Theme.of( + context, + ).colorScheme.onSurface.withAlpha(120), showSelectedLabels: true, showUnselectedLabels: true, items: navItems, @@ -74,6 +82,10 @@ class _MainScreenState extends State { if (_todoPageKey.currentState != null) { _todoPageKey.currentState!.showAddDialog(); } + } else if (index == 2) { + if (_calendarPageKey.currentState != null) { + _calendarPageKey.currentState!.showAddDialog(); + } } } @@ -101,13 +113,15 @@ class _MainScreenState extends State { return FlispPage(key: _flispPageKey); case 1: return TodoPage(key: _todoPageKey); + case 2: + return CalendarPage(key: _calendarPageKey); default: return FlispPage(key: _flispPageKey); } } String _getAppBarTitle(int index) { - final titles = {0: '闪灵', 1: '待办'}; + final titles = {0: '闪灵', 1: '待办', 2: '日程'}; return titles[index] ?? '闪灵'; } } diff --git a/lib/main.dart b/lib/main.dart index 7196302..b8f0be4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,11 +1,14 @@ +import 'package:flisp_app/models/calendar.dart'; import 'package:flisp_app/models/flisp.dart'; import 'package:flisp_app/provider/app_provider.dart'; +import 'package:flisp_app/provider/calendar_provider.dart'; import 'package:flisp_app/provider/flisp_provider.dart'; import 'package:flisp_app/utils/notify_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:hive_flutter/adapters.dart'; import 'package:provider/provider.dart'; +import 'package:syncfusion_localizations/syncfusion_localizations.dart'; import 'layout/main_screen.dart'; import 'models/todo.dart'; @@ -23,6 +26,7 @@ void main() async { // 注册适配器 Hive.registerAdapter(TodoAdapter()); Hive.registerAdapter(FlispAdapter()); + Hive.registerAdapter(CalendarAdapter()); // // 打开Box // if (Hive.isBoxOpen('flisps')) { @@ -36,6 +40,9 @@ void main() async { final flispBox = await Hive.openBox('flisps'); // flispBox.clear(); + final calendarBox = await Hive.openBox('calendars'); + // calendarBox.clear(); + // notifyService.cancelAllNotifications(); runApp(const MyApp()); @@ -51,6 +58,7 @@ class MyApp extends StatelessWidget { ChangeNotifierProvider(create: (_) => AppProvider()), ChangeNotifierProvider(create: (_) => TodoProvider()), ChangeNotifierProvider(create: (_) => FlispProvider()), + ChangeNotifierProvider(create: (_) => CalendarProvider()), ], child: Consumer( builder: (context, appProvider, child) { @@ -60,6 +68,7 @@ class MyApp extends StatelessWidget { GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, + SfGlobalLocalizations.delegate, ], supportedLocales: [const Locale('zh'), const Locale('zh', 'CN')], locale: Locale('zh', 'CN'), diff --git a/lib/models/calendar.dart b/lib/models/calendar.dart new file mode 100644 index 0000000..83088bf --- /dev/null +++ b/lib/models/calendar.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:hive/hive.dart'; + +// flutter packages pub run build_runner build +part 'calendar.g.dart'; + +@HiveType(typeId: 3) +class Calendar extends HiveObject { + @HiveField(0) + late int id; + + @HiveField(1) + late String title; + + @HiveField(2) + late DateTime? startTime; + + @HiveField(3) + late DateTime? endTime; + + @HiveField(4) + late DateTime? scheduledTime; + + @HiveField(5) + late DateTime createTime; + + @HiveField(6) + late DateTime updateTime; + + Calendar({ + int? id, + required this.title, + required this.startTime, + required this.endTime, + this.scheduledTime, + DateTime? createTime, + DateTime? updateTime, + }) { + // 简单时间戳ID + this.id = id ?? DateTime.now().millisecondsSinceEpoch ~/ 1000; + this.createTime = createTime ?? DateTime.now(); + this.updateTime = updateTime ?? DateTime.now(); + } + + static Calendar getEmpty() { + return Calendar( + id: DateTime.now().millisecondsSinceEpoch ~/ 1000, + title: '', + startTime: null, + endTime: null, + scheduledTime: null, + ); + } +} diff --git a/lib/models/calendar.g.dart b/lib/models/calendar.g.dart new file mode 100644 index 0000000..4643fe6 --- /dev/null +++ b/lib/models/calendar.g.dart @@ -0,0 +1,59 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'calendar.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class CalendarAdapter extends TypeAdapter { + @override + final int typeId = 3; + + @override + Calendar read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return Calendar( + id: fields[0] as int?, + title: fields[1] as String, + startTime: fields[2] as DateTime?, + endTime: fields[3] as DateTime?, + scheduledTime: fields[4] as DateTime?, + createTime: fields[5] as DateTime?, + updateTime: fields[6] as DateTime?, + ); + } + + @override + void write(BinaryWriter writer, Calendar obj) { + writer + ..writeByte(7) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.title) + ..writeByte(2) + ..write(obj.startTime) + ..writeByte(3) + ..write(obj.endTime) + ..writeByte(4) + ..write(obj.scheduledTime) + ..writeByte(5) + ..write(obj.createTime) + ..writeByte(6) + ..write(obj.updateTime); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is CalendarAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/pages/calendar_page.dart b/lib/pages/calendar_page.dart new file mode 100644 index 0000000..17ccf47 --- /dev/null +++ b/lib/pages/calendar_page.dart @@ -0,0 +1,175 @@ +import 'package:flisp_app/models/calendar.dart'; +import 'package:flisp_app/provider/calendar_provider.dart'; +import 'package:flisp_app/service/calendar_service.dart'; +import 'package:flisp_app/utils/notify_utils.dart'; +import 'package:flisp_app/widgets/awesome_dialog.dart'; +import 'package:flisp_app/widgets/calendar_form.dart'; +import 'package:flisp_app/widgets/calendar_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:provider/provider.dart'; +import 'package:syncfusion_flutter_calendar/calendar.dart'; + +class CalendarPage extends StatefulWidget { + const CalendarPage({super.key}); + + @override + CalendarPageState createState() => CalendarPageState(); +} + +class CalendarPageState extends State { + // 日历控制器 + final CalendarController _calendarController = CalendarController(); + final CalendarService calendarService = CalendarService(); + final NotifyService notifyService = NotifyService(); + late List _appointments; + + void showAddDialog() { + _showDialog(false, null); + } + + @override + void initState() { + super.initState(); + refreshAppointments(); + } + + void refreshAppointments() { + final calendars = calendarService.getAllCalendars(); + _appointments = + calendars + .map( + (calendar) => Appointment( + id: calendar.id, + startTime: calendar.startTime!, + endTime: calendar.endTime!, + subject: calendar.title, + color: Colors.blue, + ), + ) + .toList(); + } + + void _showDialog(bool isEditing, Calendar? calendar) { + final provider = Provider.of(context, listen: false); + + if (isEditing) { + provider.initForm(calendar!); + } else { + provider.resetForm(); + } + + final formKey = GlobalKey(); + + showAwesomeDialog( + context: context, + body: CalendarForm( + formKey: formKey, + isEditing: isEditing, + initialCalendar: calendar, + ), + onOk: () { + if (formKey.currentState!.saveAndValidate()) { + Navigator.of(context).pop(); + _saveCalendar(isEditing, provider.formItem); + } + }, + onCancel: () { + provider.resetForm(); + }, + ); + } + + void _saveCalendar(bool isEditing, Calendar calendar) async { + late bool isSuccess; + if (isEditing) { + // 先删除 + await notifyService.cancelNotification(calendar.id); + await calendarService.deleteCalendar(calendar); + // 在重新新建 + calendar.id = DateTime.now().millisecondsSinceEpoch ~/ 1000; + } + + isSuccess = await calendarService.addCalendar(calendar); + + if (calendar.scheduledTime != null) { + await notifyService.scheduleNotification( + id: calendar.id, + title: '日程提醒', + body: calendar.title, + scheduledTime: calendar.scheduledTime!, + ); + } + + setState(() { + refreshAppointments(); + }); + + if (isSuccess) { + showSuccessDialog(context, isEditing ? '更新成功' : '添加成功'); + } else { + showErrorDialog(context, isEditing ? '更新失败' : '添加失败'); + } + } + + void _deleteCalendar(Calendar calendar) async { + await notifyService.cancelNotification(calendar.id); + bool isSuccess = await calendarService.deleteCalendar(calendar); + + setState(() { + refreshAppointments(); + }); + + if (isSuccess) { + showSuccessDialog(context, '删除成功'); + } else { + showErrorDialog(context, '删除失败'); + } + } + + @override + Widget build(BuildContext context) { + final provider = Provider.of(context, listen: false); + + return Scaffold( + appBar: AppBar( + title: Text('日程安排'), + centerTitle: true, + actions: [ + buildCalendarMenu( + onSelected: (CalendarView value) { + setState(() { + _calendarController.view = value; + }); + }, + ), + ], + ), + body: Padding( + padding: EdgeInsets.all(10), + child: buildCalendar( + context: context, + controller: _calendarController, + dataSource: AppointmentDataSource(_appointments), + onAdd: () { + if (_calendarController.view == CalendarView.week) { + _showDialog(true, provider.formItem); + } + }, + onEdit: (appointment) { + final calendar = calendarService.getCalendarById( + appointment.id as int, + ); + _showDialog(true, calendar); + }, + onDelete: (appointment) { + final calendar = calendarService.getCalendarById( + appointment.id as int, + ); + _deleteCalendar(calendar); + }, + ), + ), + ); + } +} diff --git a/lib/pages/todo_page.dart b/lib/pages/todo_page.dart index f53072d..f5ee3ef 100644 --- a/lib/pages/todo_page.dart +++ b/lib/pages/todo_page.dart @@ -136,8 +136,8 @@ class TodoPageState extends State { if (todo.scheduledTime != null) { await notifyService.scheduleNotification( id: todo.id, - title: todo.title, - body: todo.content, + title: '待办提醒', + body: todo.title, scheduledTime: todo.scheduledTime!, ); } diff --git a/lib/provider/calendar_provider.dart b/lib/provider/calendar_provider.dart new file mode 100644 index 0000000..c0545a0 --- /dev/null +++ b/lib/provider/calendar_provider.dart @@ -0,0 +1,25 @@ +import 'package:flisp_app/models/calendar.dart'; +import 'package:flutter/material.dart'; + +class CalendarProvider with ChangeNotifier { + late Calendar _formItem; + + Calendar get formItem => _formItem; + + void resetForm() { + _formItem = Calendar.getEmpty(); + notifyListeners(); + } + + void initForm(Calendar calendar) { + _formItem = calendar; + notifyListeners(); + } + + void initFormByDate(DateTime date) { + _formItem = Calendar.getEmpty(); + _formItem.startTime = date; + _formItem.endTime = date.add(Duration(minutes: 30)); + notifyListeners(); + } +} diff --git a/lib/service/calendar_service.dart b/lib/service/calendar_service.dart new file mode 100644 index 0000000..878fcbf --- /dev/null +++ b/lib/service/calendar_service.dart @@ -0,0 +1,43 @@ +import 'package:flisp_app/models/calendar.dart'; +import 'package:hive_flutter/hive_flutter.dart'; + +class CalendarService { + static const String boxName = 'calendars'; + + Box get box => Hive.box(boxName); + + Future addCalendar(Calendar calendar) async { + try { + await box.add(calendar); + return true; + } catch (e) { + return false; + } + } + + List getAllCalendars() { + return box.values.toList(); + } + + Calendar getCalendarById(int id) { + return box.values.firstWhere((calendar) => calendar.id == id); + } + + Future updateCalendar(Calendar calendar) async { + try { + await calendar.save(); + return true; + } catch (e) { + return false; + } + } + + Future deleteCalendar(Calendar calendar) async { + try { + await calendar.delete(); + return true; + } catch (e) { + return false; + } + } +} diff --git a/lib/utils/date_utils.dart b/lib/utils/date_utils.dart index 378e608..e882ead 100644 --- a/lib/utils/date_utils.dart +++ b/lib/utils/date_utils.dart @@ -1,7 +1,9 @@ -// String formatDate(DateTime date) { -// return '${date.month}月${date.day}日'; -// } -// -// String formatTime(DateTime datetime) { -// return '${datetime.year}-${datetime.month}-${datetime.day} ${datetime.hour}:${datetime.minute}:${datetime.second}'; -// } +import 'package:intl/intl.dart'; + +String formatDate(DateTime date) { + return DateFormat('yyyy-MM-dd').format(date); +} + +String formatTime(DateTime datetime) { + return DateFormat('yyyy-MM-dd HH:mm:ss').format(datetime); +} diff --git a/lib/utils/notify_utils.dart b/lib/utils/notify_utils.dart index 0afb9a6..e276f50 100644 --- a/lib/utils/notify_utils.dart +++ b/lib/utils/notify_utils.dart @@ -95,7 +95,6 @@ class NotifyService { required String body, required DateTime scheduledTime, }) async { - print("scheduleNotification: $id"); await _notifications.zonedSchedule( id, title, @@ -110,8 +109,11 @@ class NotifyService { // 取消特定通知 Future cancelNotification(int id) async { - print("cancelNotification: $id"); - await _notifications.cancel(id); + try { + await _notifications.cancel(id); + } catch(e) { + print('取消通知失败: $e'); + } } // 取消所有通知 diff --git a/lib/widgets/calendar_form.dart b/lib/widgets/calendar_form.dart new file mode 100644 index 0000000..91d155d --- /dev/null +++ b/lib/widgets/calendar_form.dart @@ -0,0 +1,291 @@ +import 'package:flisp_app/models/calendar.dart'; +import 'package:flisp_app/provider/calendar_provider.dart'; +import 'package:flisp_app/utils/date_utils.dart'; +import 'package:flisp_app/widgets/common.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; + +class CalendarForm extends StatefulWidget { + final GlobalKey formKey; + final bool isEditing; + final Calendar? initialCalendar; + + const CalendarForm({ + super.key, + required this.formKey, + required this.isEditing, + this.initialCalendar, + }); + + @override + State createState() => _CalendarFormState(); +} + +class _CalendarFormState extends State { + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + final int maxTitleCount = 10; + final provider = Provider.of(context); + final colors = Theme.of(context).colorScheme; + + Widget buildTitle() { + return Row( + children: [ + Icon( + widget.isEditing ? Icons.edit_note : Icons.add_task, + color: colors.primary, + size: 24, + ), + SizedBox(width: 8), + Text( + widget.isEditing ? '编辑日程事项' : '添加日程事项', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: colors.primary, + ), + ), + ], + ); + } + + String? titleFieldValidator(value) { + if (value == null || value.isEmpty) { + return '请输入标题'; + } + if (value.length > maxTitleCount) { + return '标题不能超过$maxTitleCount个字符'; + } + return null; + } + + FormBuilderTextField buildTitleField() { + return FormBuilderTextField( + name: 'title', + initialValue: provider.formItem.title, + onChanged: (value) { + setState(() { + provider.formItem.title = value ?? ''; + }); + }, + decoration: InputDecoration( + label: RichText( + text: TextSpan( + text: '内容', + style: TextStyle(color: Colors.grey.shade700, fontSize: 16), + children: const [ + TextSpan( + text: '*', + style: TextStyle( + color: Colors.red, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + hintText: '请输入日程标题...', + hintStyle: TextStyle(color: Colors.grey), + counterText: '', + suffixText: '${provider.formItem.title.length}/$maxTitleCount', + border: buildFormBoard(), + enabledBorder: buildFormEnabledBoard(), + focusedBorder: buildFormFocusedBoard(colors), + filled: true, + fillColor: colors.surface, + prefixIcon: Icon(Icons.title, color: Colors.blue), + contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), + ), + maxLength: maxTitleCount, + validator: (value) => titleFieldValidator(value), + ); + } + + String? startTimeFieldValidator(value) { + if (value == null) { + return '请选择开始时间'; + } + if (value.isBefore(DateTime.now())) { + return '不能选择过去的日期'; + } + + final endTime = provider.formItem.endTime; + if (endTime != null && endTime.isBefore(value)) { + return '结束时间不能早于开始时间'; + } + if (endTime != null && endTime.isAtSameMomentAs(value)) { + return '开始时间不能等于结束时间'; + } + + return null; + } + + FormBuilderDateTimePicker buildStartTimeField() { + var startTime = provider.formItem.startTime; + + return FormBuilderDateTimePicker( + name: 'startTime', + format: DateFormat('yyyy-MM-dd HH:mm:ss'), + initialValue: startTime, + inputType: InputType.both, + onChanged: (value) { + setState(() { + provider.formItem.startTime = value!; + }); + }, + decoration: InputDecoration( + labelText: + startTime == null ? '选择开始时间' : '开始时间: ${formatTime(startTime)}', + labelStyle: TextStyle( + color: startTime == null ? Colors.grey : Colors.black87, + ), + prefixIcon: Icon(Icons.calendar_month, color: Colors.purple), + border: buildFormBoard(), + enabledBorder: buildFormEnabledBoard(), + focusedBorder: buildFormFocusedBoard(colors), + filled: true, + fillColor: colors.surface, + contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), + ), + validator: (value) => startTimeFieldValidator(value), + ); + } + + String? endTimeFieldValidator(value) { + if (value == null) { + return '请选择结束时间'; + } + if (value.isBefore(DateTime.now())) { + return '不能选择过去的日期'; + } + + final startTime = provider.formItem.startTime; + if (startTime != null && value.isBefore(startTime)) { + return '结束时间不能早于开始时间'; + } + if (startTime != null && startTime.isAtSameMomentAs(value)) { + return '开始时间不能等于结束时间'; + } + + return null; + } + + FormBuilderDateTimePicker buildEndTimeField() { + var endTime = provider.formItem.endTime; + + return FormBuilderDateTimePicker( + name: 'endTime', + format: DateFormat('yyyy-MM-dd HH:mm:ss'), + initialValue: endTime, + inputType: InputType.both, + onChanged: (value) { + setState(() { + provider.formItem.endTime = value!; + }); + }, + decoration: InputDecoration( + labelText: + endTime == null ? '选择结束时间' : '结束时间: ${formatTime(endTime)}', + labelStyle: TextStyle( + color: endTime == null ? Colors.grey : Colors.black87, + ), + prefixIcon: Icon(Icons.calendar_month, color: Colors.purple), + border: buildFormBoard(), + enabledBorder: buildFormEnabledBoard(), + focusedBorder: buildFormFocusedBoard(colors), + filled: true, + fillColor: colors.surface, + contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), + ), + validator: (value) => endTimeFieldValidator(value), + ); + } + + IconButton buildClearScheduled() { + return IconButton( + icon: Icon(Icons.clear, size: 18), + onPressed: () { + setState(() { + provider.formItem.scheduledTime = null; + }); + widget.formKey.currentState?.fields['scheduledTime']?.didChange(null); + }, + ); + } + + String? scheduledTimeFieldValidator(value) { + if (value != null && value.isBefore(DateTime.now())) { + return '不能选择过去的日期'; + } + return null; + } + + FormBuilderDateTimePicker buildScheduledTimeField() { + var scheduledTime = provider.formItem.scheduledTime; + + return FormBuilderDateTimePicker( + name: 'scheduledTime', + format: DateFormat('yyyy-MM-dd HH:mm:ss'), + initialValue: scheduledTime, + inputType: InputType.both, + onChanged: (value) { + setState(() { + provider.formItem.scheduledTime = value; + }); + }, + decoration: InputDecoration( + labelText: + scheduledTime == null + ? '选择提醒日期' + : '提醒: ${formatTime(scheduledTime)}', + labelStyle: TextStyle( + color: scheduledTime == null ? Colors.grey : Colors.black87, + ), + prefixIcon: Icon(Icons.calendar_month, color: Colors.purple), + border: buildFormBoard(), + enabledBorder: buildFormEnabledBoard(), + focusedBorder: buildFormFocusedBoard(colors), + filled: true, + fillColor: colors.surface, + contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), + suffixIcon: scheduledTime != null ? buildClearScheduled() : null, + ), + validator: (value) => scheduledTimeFieldValidator(value), + ); + } + + FormBuilder buildForm() { + return FormBuilder( + key: widget.formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + buildTitleField(), + SizedBox(height: 12), + buildStartTimeField(), + SizedBox(height: 12), + buildEndTimeField(), + SizedBox(height: 12), + buildScheduledTimeField(), + ], + ), + ); + } + + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [buildTitle(), SizedBox(height: 20), buildForm()], + ), + ); + } +} diff --git a/lib/widgets/calendar_widget.dart b/lib/widgets/calendar_widget.dart new file mode 100644 index 0000000..7f589e3 --- /dev/null +++ b/lib/widgets/calendar_widget.dart @@ -0,0 +1,89 @@ +import 'package:flisp_app/provider/calendar_provider.dart'; +import 'package:flisp_app/widgets/common.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:syncfusion_flutter_calendar/calendar.dart'; + +final viewItems = [ + {'value': CalendarView.week, 'label': '周视图'}, + {'value': CalendarView.month, 'label': '月视图'}, + {'value': CalendarView.schedule, 'label': '日程视图'}, +]; + +class AppointmentDataSource extends CalendarDataSource { + AppointmentDataSource(List source) { + appointments = source; + } +} + +Widget buildCalendarMenu({required void Function(CalendarView) onSelected}) { + return PopupMenuButton( + onSelected: onSelected, + itemBuilder: (BuildContext context) { + return viewItems.map((item) { + return PopupMenuItem( + value: item['value'] as CalendarView, + child: Text(item['label'].toString()), + ); + }).toList(); + }, + ); +} + +Widget buildCalendar({ + required BuildContext context, + required CalendarController controller, + required CalendarDataSource dataSource, + required VoidCallback onAdd, + required ValueChanged onEdit, + required ValueChanged onDelete, +}) { + final provider = Provider.of(context); + + return SfCalendar( + view: CalendarView.week, + controller: controller, + dataSource: dataSource, + firstDayOfWeek: 1, + showDatePickerButton: true, + showNavigationArrow: true, + allowViewNavigation: true, + // 月份视图设置 + monthViewSettings: MonthViewSettings( + appointmentDisplayMode: MonthAppointmentDisplayMode.appointment, + showAgenda: true, + ), + // 时间区域设置 + timeSlotViewSettings: TimeSlotViewSettings( + startHour: 7, + endHour: 23, + timeFormat: 'HH:mm', + timeInterval: Duration(minutes: 30), + timeRulerSize: 60, + ), + // 选择日期回调 + onTap: (CalendarTapDetails details) { + if (details.targetElement == CalendarElement.calendarCell) { + if (!details.date!.isBefore(DateTime.now())) { + provider.initFormByDate(details.date!); + onAdd(); + } + } else if (details.targetElement == CalendarElement.appointment) { + if (details.appointments?.length == 1) { + onEdit(details.appointments!.first); + } + } + }, + // 选择日程回调 + onLongPress: (CalendarLongPressDetails details) async { + if (details.targetElement == CalendarElement.appointment) { + if (details.appointments?.length == 1) { + final bool? result = await showDeleteConfirmationDialog(context); + if (result == true) { + onDelete(details.appointments!.first); + } + } + } + }, + ); +} diff --git a/lib/widgets/common.dart b/lib/widgets/common.dart index d0f1b2b..f608ef4 100644 --- a/lib/widgets/common.dart +++ b/lib/widgets/common.dart @@ -1,11 +1,24 @@ import 'package:flutter/material.dart'; import 'package:toggle_switch/toggle_switch.dart'; -BoxDecoration buildBoxDecoration() { - return BoxDecoration( - color: Colors.white, +OutlineInputBorder buildFormBoard() { + return OutlineInputBorder( borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.grey[200]!, width: 1), + borderSide: BorderSide(color: Colors.grey.shade200), + ); +} + +OutlineInputBorder buildFormEnabledBoard() { + return OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade200), + ); +} + +OutlineInputBorder buildFormFocusedBoard(ColorScheme colors) { + return OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: colors.primary), ); } @@ -130,7 +143,7 @@ Widget buildDismissible({ background: _buildDismissBackground(), confirmDismiss: (direction) async { // 这里实现二次确认 - return await _showDeleteConfirmationDialog(context); + return await showDeleteConfirmationDialog(context); }, onDismissed: (direction) { onDelete(); @@ -140,7 +153,7 @@ Widget buildDismissible({ } // 确认对话框 -Future _showDeleteConfirmationDialog(BuildContext context) async { +Future showDeleteConfirmationDialog(BuildContext context) async { return showDialog( context: context, builder: (BuildContext context) { diff --git a/pubspec.lock b/pubspec.lock index 0936bee..720c686 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -807,6 +807,38 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.4.1" + syncfusion_flutter_calendar: + dependency: "direct main" + description: + name: syncfusion_flutter_calendar + sha256: "8e8a4eef01d6a82ae2c17e76d497ff289ded274de014c9f471ffabc12d1e2e71" + url: "https://pub.flutter-io.cn" + source: hosted + version: "30.2.7" + syncfusion_flutter_core: + dependency: transitive + description: + name: syncfusion_flutter_core + sha256: bfd026c0f9822b49ff26fed11cd3334519acb6a6ad4b0c81d9cd18df6af1c4c0 + url: "https://pub.flutter-io.cn" + source: hosted + version: "30.2.7" + syncfusion_flutter_datepicker: + dependency: transitive + description: + name: syncfusion_flutter_datepicker + sha256: b5f35cc808e91b229d41613efe71dadab1549a35bfd493f922fc06ccc2fe908c + url: "https://pub.flutter-io.cn" + source: hosted + version: "30.2.7" + syncfusion_localizations: + dependency: "direct main" + description: + name: syncfusion_localizations + sha256: bb32b07879b4c1dee5d4c8ad1c57343a4fdae55d65a87f492727c11b68f23164 + url: "https://pub.flutter-io.cn" + source: hosted + version: "30.2.7" term_glyph: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9399e0f..a6a131d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -44,6 +44,8 @@ dependencies: file_picker: ^10.3.3 minio: ^3.5.8 crypto: ^3.0.7 + syncfusion_flutter_calendar: ^30.1.37 + syncfusion_localizations: ^30.1.37 dev_dependencies: flutter_test: