diff --git a/apps/lib/features/calendar/ui/screens/calendar_dayweek_screen.dart b/apps/lib/features/calendar/ui/screens/calendar_dayweek_screen.dart index 291890e..0e52cc9 100644 --- a/apps/lib/features/calendar/ui/screens/calendar_dayweek_screen.dart +++ b/apps/lib/features/calendar/ui/screens/calendar_dayweek_screen.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:lucide_icons/lucide_icons.dart'; +import '../../../../core/router/app_routes.dart'; +import '../../../home/ui/navigation/home_return_policy.dart'; import '../../../../core/di/injection.dart'; import '../../../../core/theme/design_tokens.dart'; @@ -14,7 +16,6 @@ import '../dayweek/day_event_layout_engine.dart'; import '../dayweek/day_timeline_metrics.dart'; import '../dayweek/day_view_scale.dart'; import '../widgets/bottom_dock.dart'; -import '../widgets/create_event_sheet.dart'; class CalendarDayWeekScreen extends StatefulWidget { final DateTime? initialDate; @@ -118,7 +119,7 @@ class _CalendarDayWeekScreenState extends State canPop: false, onPopInvokedWithResult: (didPop, result) { if (!didPop) { - context.go('/home'); + returnToHomePreserveState(context); } }, child: SafeArea( @@ -313,10 +314,8 @@ class _CalendarDayWeekScreenState extends State if (isNotToday) const SizedBox(width: 8), AppPressable( borderRadius: BorderRadius.circular(AppRadius.full), - onTap: () => CreateEventSheet.show( - context, - initialDate: _selectedDate, - onSaved: _loadEvents, + onTap: () => context.push( + '${AppRoutes.calendarEventCreate}?date=${formatYmd(_selectedDate)}', ), child: Container( width: 36, @@ -636,7 +635,8 @@ class _CalendarDayWeekScreenState extends State height: tapHeight, child: GestureDetector( behavior: HitTestBehavior.translucent, - onTap: () => context.push('/calendar/events/${layout.event.id}'), + onTap: () => + context.push(AppRoutes.calendarEventDetail(layout.event.id)), child: Stack( children: [ Positioned( @@ -696,13 +696,13 @@ class _CalendarDayWeekScreenState extends State activeTab: DockTab.calendar, onTodoTap: () { _calendarManager.setViewType(CalendarViewType.day); - context.go('/todo'); + context.push(AppRoutes.todoList); }, onCalendarTap: () { _calendarManager.setViewType(CalendarViewType.day); - context.go('/calendar/month'); + context.push(AppRoutes.calendarMonth); }, - onHomeTap: () => context.go('/home'), + onHomeTap: () => returnToHomePreserveState(context), ); } } diff --git a/apps/lib/features/calendar/ui/screens/calendar_event_create_screen.dart b/apps/lib/features/calendar/ui/screens/calendar_event_create_screen.dart new file mode 100644 index 0000000..10e08ea --- /dev/null +++ b/apps/lib/features/calendar/ui/screens/calendar_event_create_screen.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +import '../../../../core/theme/design_tokens.dart'; +import '../widgets/create_event_sheet.dart'; + +class CalendarEventCreateScreen extends StatelessWidget { + final DateTime? initialDate; + + const CalendarEventCreateScreen({super.key, this.initialDate}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.background, + body: SafeArea( + child: CreateEventSheet(initialDate: initialDate, pageMode: true), + ), + ); + } +} diff --git a/apps/lib/features/calendar/ui/screens/calendar_event_detail_screen.dart b/apps/lib/features/calendar/ui/screens/calendar_event_detail_screen.dart index 9eb8aee..7165744 100644 --- a/apps/lib/features/calendar/ui/screens/calendar_event_detail_screen.dart +++ b/apps/lib/features/calendar/ui/screens/calendar_event_detail_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:lucide_icons/lucide_icons.dart'; import 'package:go_router/go_router.dart'; import '../../../../core/di/injection.dart'; +import '../../../../core/router/app_routes.dart'; import '../../../../core/notifications/local_notification_service.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_loading_indicator.dart'; @@ -11,8 +12,6 @@ import '../../../../shared/widgets/destructive_action_sheet.dart'; import '../../data/services/calendar_service.dart'; import '../../data/models/schedule_item_model.dart'; import '../utils/event_color_resolver.dart'; -import '../widgets/create_event_sheet.dart'; -import '../widgets/calendar_share_dialog.dart'; enum _CalendarHeaderAction { edit, delete, share } @@ -58,34 +57,50 @@ class _CalendarEventDetailScreenState extends State { } if (_event == null) { return Scaffold( - backgroundColor: const Color(0xFFF8FAFC), + backgroundColor: AppColors.background, body: SafeArea( - child: Center( + child: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [AppColors.homeBackgroundTop, AppColors.background], + ), + ), child: Column( - mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const Text( - 'Event not found', - style: TextStyle(color: AppColors.slate600), - ), - const SizedBox(height: 16), - SizedBox( - height: AppSpacing.xxl * 2, - child: TextButton( - onPressed: () => context.pop(), - style: TextButton.styleFrom( + const BackTitlePageHeader(title: '日程详情'), + Expanded( + child: Center( + child: Padding( padding: const EdgeInsets.symmetric( - horizontal: AppSpacing.lg, + horizontal: AppSpacing.xl, ), - backgroundColor: AppColors.blue600, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.full), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text( + '未找到该日程', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.slate700, + ), + ), + const SizedBox(height: AppSpacing.sm), + const Text( + '可能已被删除,或你没有访问权限。', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 13, + color: AppColors.slate500, + ), + ), + ], ), ), - child: const Text( - '返回', - style: TextStyle(color: AppColors.white), - ), ), ), ], @@ -97,15 +112,41 @@ class _CalendarEventDetailScreenState extends State { final event = _event!; return Scaffold( - backgroundColor: const Color(0xFFF8FAFC), + backgroundColor: AppColors.background, body: SafeArea( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _buildHeader(event), - Expanded(child: _buildDetailOverlay(event)), - _buildInputContainer(), - ], + child: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [AppColors.homeBackgroundTop, AppColors.background], + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildHeader(event), + Expanded( + child: ListView( + padding: const EdgeInsets.fromLTRB( + AppSpacing.lg, + AppSpacing.sm, + AppSpacing.lg, + AppSpacing.xl, + ), + children: [ + _buildHeroSurface(event), + const SizedBox(height: AppSpacing.md), + _buildMetaSurface(event), + if (_hasExtraInfo(event)) ...[ + const SizedBox(height: AppSpacing.md), + _buildExtraSurface(event), + ], + ], + ), + ), + ], + ), ), ), ); @@ -123,7 +164,7 @@ class _CalendarEventDetailScreenState extends State { final items = >[]; if (event.canEdit) { items.add( - const DetailHeaderActionItem<_CalendarHeaderAction>( + DetailHeaderActionItem<_CalendarHeaderAction>( value: _CalendarHeaderAction.edit, label: '编辑', icon: LucideIcons.pencil, @@ -132,7 +173,7 @@ class _CalendarEventDetailScreenState extends State { } if (event.canDelete) { items.add( - const DetailHeaderActionItem<_CalendarHeaderAction>( + DetailHeaderActionItem<_CalendarHeaderAction>( value: _CalendarHeaderAction.delete, label: '删除', icon: LucideIcons.trash2, @@ -142,7 +183,7 @@ class _CalendarEventDetailScreenState extends State { } if (event.canInvite) { items.add( - const DetailHeaderActionItem<_CalendarHeaderAction>( + DetailHeaderActionItem<_CalendarHeaderAction>( value: _CalendarHeaderAction.share, label: '分享', icon: LucideIcons.share2, @@ -162,85 +203,220 @@ class _CalendarEventDetailScreenState extends State { ) { switch (action) { case _CalendarHeaderAction.edit: - CreateEventSheet.edit( - context, - event, - onSaved: () { - setState(() { - _loadEvent(); - }); - }, - ); + context.push(AppRoutes.calendarEventEdit(event.id)).then((_) { + _loadEvent(); + }); return; case _CalendarHeaderAction.delete: _showDeleteConfirmation(); return; case _CalendarHeaderAction.share: - CalendarShareDialog.show( - context, - event.id, - event.title, - canInvite: event.canInvite, - canEdit: event.canEdit, - ); + context.push(AppRoutes.calendarEventShare(event.id)); return; } } - Widget _buildDetailOverlay(ScheduleItemModel event) { - final startAt = event.startAt; - final endAt = event.endAt; - final dateStr = - '${startAt.year}年${startAt.month}月${startAt.day}日 ${_getWeekday(startAt.weekday)}'; - final timeStr = endAt != null - ? '${_formatTime(startAt)} - ${_formatTime(endAt)}' - : _formatTime(startAt); + Widget _buildHeroSurface(ScheduleItemModel event) { + final color = resolveEventColor( + status: event.status, + colorHex: event.metadata?.color, + ); + final timeRange = _formatRangeLabel(event.startAt, event.endAt); - return Padding( - padding: const EdgeInsets.only(left: 16, right: 16, top: 8, bottom: 12), - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(24), - border: Border.all(color: const Color(0xFFD8E3F5)), - ), - child: SingleChildScrollView( - child: SizedBox( + return Container( + padding: const EdgeInsets.all(AppSpacing.lg), + decoration: BoxDecoration( + color: AppColors.white, + borderRadius: BorderRadius.circular(AppRadius.xl), + border: Border.all(color: AppColors.borderTertiary), + boxShadow: [ + BoxShadow( + color: AppColors.slate200.withValues(alpha: 0.38), + blurRadius: AppRadius.lg, + offset: const Offset(0, AppSpacing.xs), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: AppSpacing.sm, + height: AppSpacing.xxl, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(AppRadius.full), + ), + ), + const SizedBox(width: AppSpacing.md), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + event.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + color: AppColors.slate900, + ), + ), + const SizedBox(height: AppSpacing.xs), + _buildStatusBadge(event.status), + ], + ), + ), + ], + ), + const SizedBox(height: AppSpacing.md), + Container( width: double.infinity, + padding: const EdgeInsets.all(AppSpacing.md), + decoration: BoxDecoration( + color: AppColors.surfaceInfoLight, + borderRadius: BorderRadius.circular(AppRadius.lg), + border: Border.all(color: AppColors.borderQuaternary), + ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildTitleRow(event), - const SizedBox(height: 14), - Container(height: 1, color: const Color(0xFFE5E7EB)), - const SizedBox(height: 14), - _buildDetailField('日期', dateStr), - const SizedBox(height: 14), - _buildDetailField('时间范围', timeStr), - const SizedBox(height: 14), - _buildDetailField( - '提醒时间', - _formatReminderText(event.metadata?.reminderMinutes), + const Text( + '时间安排', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: AppColors.slate500, + ), + ), + const SizedBox(height: AppSpacing.xs), + Text( + timeRange, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w700, + color: AppColors.slate800, + ), ), - const SizedBox(height: 14), - _buildColorField(event), - const SizedBox(height: 14), - if (event.metadata?.location != null) ...[ - _buildDetailField('地点', event.metadata!.location!), - const SizedBox(height: 14), - ], - if (event.description != null) ...[ - _buildDetailField('描述', event.description!), - const SizedBox(height: 14), - ], - if (event.metadata?.notes != null) ...[ - _buildNotesField(event.metadata!.notes!), - ], ], ), ), - ), + ], + ), + ); + } + + Widget _buildMetaSurface(ScheduleItemModel event) { + final startAt = event.startAt; + final dateStr = + '${startAt.year}年${startAt.month}月${startAt.day}日 ${_getWeekday(startAt.weekday)}'; + final color = resolveEventColor( + status: event.status, + colorHex: event.metadata?.color, + ); + + return Container( + padding: const EdgeInsets.all(AppSpacing.lg), + decoration: BoxDecoration( + color: AppColors.white, + borderRadius: BorderRadius.circular(AppRadius.xl), + border: Border.all(color: AppColors.borderSecondary), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '基础信息', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: AppColors.slate500, + ), + ), + const SizedBox(height: AppSpacing.md), + _buildDetailRow('日期', dateStr), + const SizedBox(height: AppSpacing.md), + _buildDetailRow( + '提醒', + _formatReminderText(event.metadata?.reminderMinutes), + ), + const SizedBox(height: AppSpacing.md), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox( + width: AppSpacing.xxl * 3, + child: Text( + '颜色', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: AppColors.slate500, + ), + ), + ), + Container( + width: AppSpacing.xl, + height: AppSpacing.xl, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + border: Border.all(color: AppColors.borderSecondary), + ), + ), + ], + ), + ], + ), + ); + } + + bool _hasExtraInfo(ScheduleItemModel event) { + return (event.metadata?.location?.trim().isNotEmpty ?? false) || + (event.description?.trim().isNotEmpty ?? false) || + (event.metadata?.notes?.trim().isNotEmpty ?? false); + } + + Widget _buildExtraSurface(ScheduleItemModel event) { + return Container( + padding: const EdgeInsets.all(AppSpacing.lg), + decoration: BoxDecoration( + color: AppColors.surfaceSecondary, + borderRadius: BorderRadius.circular(AppRadius.xl), + border: Border.all(color: AppColors.borderSecondary), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '补充信息', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: AppColors.slate500, + ), + ), + if (event.metadata?.location?.trim().isNotEmpty ?? false) ...[ + const SizedBox(height: AppSpacing.md), + _buildDetailRow('地点', event.metadata!.location!.trim()), + ], + if (event.description?.trim().isNotEmpty ?? false) ...[ + const SizedBox(height: AppSpacing.md), + _buildDetailRow('描述', event.description!.trim()), + ], + if (event.metadata?.notes?.trim().isNotEmpty ?? false) ...[ + const SizedBox(height: AppSpacing.md), + _buildDetailRow( + '备注', + event.metadata!.notes!.trim(), + multiline: true, + ), + ], + ], ), ); } @@ -264,44 +440,6 @@ class _CalendarEventDetailScreenState extends State { return '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}'; } - Widget _buildTitleRow(ScheduleItemModel event) { - return Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - width: 4, - height: 20, - decoration: BoxDecoration( - color: resolveEventColor( - status: event.status, - colorHex: event.metadata?.color, - ), - borderRadius: BorderRadius.circular(2), - ), - ), - const SizedBox(width: 10), - Flexible( - child: Text( - event.title, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w700, - color: AppColors.slate900, - ), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - ], - ); - } - Future _showDeleteConfirmation() async { final confirmed = await showDestructiveActionSheet( context, @@ -322,140 +460,76 @@ class _CalendarEventDetailScreenState extends State { context.pop(); } - Widget _buildDetailField(String label, String value) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: AppColors.slate400, - ), - ), - const SizedBox(height: 4), - Text( - value, - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600, - color: AppColors.slate900, - ), - ), - ], - ); + String _formatRangeLabel(DateTime startAt, DateTime? endAt) { + final dateLabel = + '${startAt.month}月${startAt.day}日 ${_getWeekday(startAt.weekday)}'; + if (endAt == null) { + return '$dateLabel ${_formatTime(startAt)}'; + } + return '$dateLabel ${_formatTime(startAt)} - ${_formatTime(endAt)}'; } - Widget _buildColorField(ScheduleItemModel event) { - final color = resolveEventColor( - status: event.status, - colorHex: event.metadata?.color, - ); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - '日程颜色', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: AppColors.slate400, - ), - ), - const SizedBox(height: 6), - Row( - children: [ - Container( - width: 28, - height: 28, - decoration: BoxDecoration(color: color, shape: BoxShape.circle), - ), - ], - ), - ], - ); - } - - Widget _buildNotesField(String notes) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - '备注', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: AppColors.slate400, - ), - ), - const SizedBox(height: 6), - Container( - width: double.infinity, - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: const Color(0xFFFDFEFF), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: const Color(0xFFDCE5F4)), - ), - child: Text( - notes, - style: const TextStyle(fontSize: 14, color: AppColors.slate700), - ), - ), - ], - ); - } - - Widget _buildInputContainer() { + Widget _buildStatusBadge(ScheduleStatus status) { + final isArchived = status == ScheduleStatus.archived; return Container( - height: 80, - padding: const EdgeInsets.all(16), - color: const Color(0xFFF8FAFC), - child: Row( - children: [ - Container( - width: 36, - height: 36, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(18), - border: Border.all(color: const Color(0xFFDCE5F4)), - ), - child: const Icon( - LucideIcons.plus, - size: 20, - color: AppColors.slate500, - ), - ), - const SizedBox(width: 8), - Expanded( - child: Container( - height: 48, - padding: const EdgeInsets.symmetric(horizontal: 16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(24), - ), - child: Row( - children: [ - const Expanded( - child: Text( - '输入消息...', - style: TextStyle(fontSize: 14, color: AppColors.slate400), - ), - ), - const Icon( - LucideIcons.mic, - size: 20, - color: AppColors.slate500, - ), - ], - ), - ), - ), - ], + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.sm, + vertical: AppSpacing.xs, + ), + decoration: BoxDecoration( + color: isArchived + ? AppColors.feedbackWarningSurface + : AppColors.feedbackSuccessSurface, + borderRadius: BorderRadius.circular(AppRadius.full), + border: Border.all( + color: isArchived + ? AppColors.feedbackWarningBorder + : AppColors.feedbackSuccessBorder, + ), + ), + child: Text( + isArchived ? '已过期' : '启用', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: isArchived + ? AppColors.feedbackWarningText + : AppColors.feedbackSuccessText, + ), ), ); } + + Widget _buildDetailRow(String label, String value, {bool multiline = false}) { + return Row( + crossAxisAlignment: multiline + ? CrossAxisAlignment.start + : CrossAxisAlignment.center, + children: [ + SizedBox( + width: AppSpacing.xxl * 3, + child: Text( + label, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: AppColors.slate500, + ), + ), + ), + const SizedBox(width: AppSpacing.sm), + Expanded( + child: Text( + value, + style: TextStyle( + fontSize: 14, + height: multiline ? 1.4 : 1.0, + fontWeight: FontWeight.w600, + color: AppColors.slate800, + ), + ), + ), + ], + ); + } } diff --git a/apps/lib/features/calendar/ui/screens/calendar_event_edit_screen.dart b/apps/lib/features/calendar/ui/screens/calendar_event_edit_screen.dart new file mode 100644 index 0000000..e43901f --- /dev/null +++ b/apps/lib/features/calendar/ui/screens/calendar_event_edit_screen.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; + +import '../../../../core/di/injection.dart'; +import '../../../../core/theme/design_tokens.dart'; +import '../../../../shared/widgets/app_loading_indicator.dart'; +import '../widgets/create_event_sheet.dart'; +import '../../data/models/schedule_item_model.dart'; +import '../../data/services/calendar_service.dart'; + +class CalendarEventEditScreen extends StatefulWidget { + final String eventId; + + const CalendarEventEditScreen({super.key, required this.eventId}); + + @override + State createState() => + _CalendarEventEditScreenState(); +} + +class _CalendarEventEditScreenState extends State { + ScheduleItemModel? _event; + bool _loading = true; + + @override + void initState() { + super.initState(); + _load(); + } + + Future _load() async { + try { + _event = await sl().getEventById(widget.eventId); + } catch (_) { + _event = null; + } + if (!mounted) { + return; + } + setState(() { + _loading = false; + }); + } + + @override + Widget build(BuildContext context) { + if (_loading) { + return const Scaffold( + body: SafeArea(child: Center(child: AppLoadingIndicator(size: 22))), + ); + } + + if (_event == null) { + return const Scaffold( + body: SafeArea(child: Center(child: Text('日程不存在或无权限'))), + ); + } + + return Scaffold( + backgroundColor: AppColors.background, + body: SafeArea( + child: CreateEventSheet(editingEvent: _event, pageMode: true), + ), + ); + } +} diff --git a/apps/lib/features/calendar/ui/screens/calendar_event_share_screen.dart b/apps/lib/features/calendar/ui/screens/calendar_event_share_screen.dart new file mode 100644 index 0000000..5c85d06 --- /dev/null +++ b/apps/lib/features/calendar/ui/screens/calendar_event_share_screen.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; + +import '../../../../core/di/injection.dart'; +import '../../../../core/theme/design_tokens.dart'; +import '../../../../shared/widgets/app_loading_indicator.dart'; +import '../../../../shared/widgets/back_title_page_header.dart'; +import '../../data/models/schedule_item_model.dart'; +import '../../data/services/calendar_service.dart'; +import '../widgets/calendar_share_dialog.dart'; + +class CalendarEventShareScreen extends StatefulWidget { + final String eventId; + + const CalendarEventShareScreen({super.key, required this.eventId}); + + @override + State createState() => + _CalendarEventShareScreenState(); +} + +class _CalendarEventShareScreenState extends State { + ScheduleItemModel? _event; + bool _loading = true; + + @override + void initState() { + super.initState(); + _load(); + } + + Future _load() async { + try { + _event = await sl().getEventById(widget.eventId); + } catch (_) { + _event = null; + } + if (!mounted) { + return; + } + setState(() { + _loading = false; + }); + } + + @override + Widget build(BuildContext context) { + if (_loading) { + return const Scaffold( + body: SafeArea(child: Center(child: AppLoadingIndicator(size: 22))), + ); + } + + final event = _event; + if (event == null) { + return const Scaffold( + body: SafeArea(child: Center(child: Text('日程不存在或无权限'))), + ); + } + + return Scaffold( + backgroundColor: AppColors.background, + body: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const BackTitlePageHeader(title: '分享日程'), + Expanded( + child: CalendarShareDialog( + eventId: event.id, + eventTitle: event.title, + canInvite: event.canInvite, + canEdit: event.canEdit, + ), + ), + ], + ), + ), + ); + } +} diff --git a/apps/lib/features/calendar/ui/screens/calendar_month_screen.dart b/apps/lib/features/calendar/ui/screens/calendar_month_screen.dart index cf71ffa..bf6355b 100644 --- a/apps/lib/features/calendar/ui/screens/calendar_month_screen.dart +++ b/apps/lib/features/calendar/ui/screens/calendar_month_screen.dart @@ -3,13 +3,14 @@ import 'package:flutter/cupertino.dart'; import 'package:go_router/go_router.dart'; import 'package:lucide_icons/lucide_icons.dart'; import '../../../../core/di/injection.dart'; +import '../../../../core/router/app_routes.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_pressable.dart'; +import '../../../home/ui/navigation/home_return_policy.dart'; import '../calendar_state_manager.dart'; import '../calendar_time_utils.dart'; import '../utils/event_color_resolver.dart'; import '../widgets/bottom_dock.dart'; -import '../widgets/create_event_sheet.dart'; import '../../data/models/schedule_item_model.dart'; import '../../data/services/calendar_service.dart'; @@ -104,7 +105,7 @@ class _CalendarMonthScreenState extends State canPop: false, onPopInvokedWithResult: (didPop, result) { if (!didPop) { - context.go('/home'); + returnToHomePreserveState(context); } }, child: SafeArea( @@ -171,10 +172,7 @@ class _CalendarMonthScreenState extends State const Spacer(), AppPressable( borderRadius: BorderRadius.circular(AppRadius.full), - onTap: () => CreateEventSheet.show( - context, - onSaved: _loadMonthEvents, - ), + onTap: () => context.push(AppRoutes.calendarEventCreate), child: Container( width: 36, height: 36, @@ -293,7 +291,9 @@ class _CalendarMonthScreenState extends State }); _calendarManager.setSelectedDate(date); _calendarManager.setViewType(CalendarViewType.month); - context.push('/calendar/dayweek?date=${formatYmd(date)}'); + context.push( + '${AppRoutes.calendarDayWeek}?date=${formatYmd(date)}', + ); }, child: AnimatedContainer( duration: const Duration(milliseconds: 140), @@ -400,7 +400,9 @@ class _CalendarMonthScreenState extends State onTap: () { _calendarManager.setSelectedDate(date); _calendarManager.setViewType(CalendarViewType.day); - context.push('/calendar/dayweek?date=${formatYmd(date)}'); + context.push( + '${AppRoutes.calendarDayWeek}?date=${formatYmd(date)}', + ); }, child: Text( '+$remainingCount', @@ -517,10 +519,10 @@ class _CalendarMonthScreenState extends State activeTab: DockTab.calendar, onTodoTap: () { _calendarManager.setViewType(CalendarViewType.month); - context.go('/todo'); + context.push(AppRoutes.todoList); }, onCalendarTap: () {}, - onHomeTap: () => context.go('/home'), + onHomeTap: () => returnToHomePreserveState(context), ); } } diff --git a/apps/lib/features/calendar/ui/widgets/create_event_sheet.dart b/apps/lib/features/calendar/ui/widgets/create_event_sheet.dart index fbb5760..1944604 100644 --- a/apps/lib/features/calendar/ui/widgets/create_event_sheet.dart +++ b/apps/lib/features/calendar/ui/widgets/create_event_sheet.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:lucide_icons/lucide_icons.dart'; import '../../../../core/di/injection.dart'; @@ -6,8 +5,10 @@ import '../../../../core/notifications/local_notification_service.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_loading_indicator.dart'; import '../../../../shared/widgets/app_sheet_input_field.dart'; +import '../../../../shared/widgets/back_title_page_header.dart'; import '../../../../shared/widgets/toast/toast.dart'; import '../../../../shared/widgets/toast/toast_type.dart'; +import 'date_time_picker_sheet.dart'; import '../../data/models/schedule_item_model.dart'; import '../../data/services/calendar_service.dart'; @@ -15,12 +16,14 @@ class CreateEventSheet extends StatefulWidget { final DateTime? initialDate; final ScheduleItemModel? editingEvent; final VoidCallback? onSaved; + final bool pageMode; const CreateEventSheet({ super.key, this.initialDate, this.editingEvent, this.onSaved, + this.pageMode = false, }); static Future show( @@ -135,6 +138,20 @@ class _CreateEventSheetState extends State @override Widget build(BuildContext context) { + if (widget.pageMode) { + return Container( + color: AppColors.background, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildPageHeader(), + _buildTabBar(), + Expanded(child: _buildTabContent()), + ], + ), + ); + } + return AnimatedPadding( duration: const Duration(milliseconds: 150), curve: Curves.easeOut, @@ -171,6 +188,44 @@ class _CreateEventSheetState extends State ); } + Widget _buildPageHeader() { + return BackTitlePageHeader( + title: _isEditing ? '编辑日程' : '新建日程', + onBack: () => Navigator.of(context).pop(), + trailing: ValueListenableBuilder( + valueListenable: _titleController, + builder: (context, value, child) { + final enabled = value.text.trim().isNotEmpty && !_saving; + return SizedBox( + height: AppSpacing.xxl * 2, + child: TextButton( + onPressed: enabled ? _saveEvent : null, + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md), + minimumSize: const Size(AppSpacing.none, AppSpacing.none), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + child: _saving + ? const AppLoadingIndicator( + variant: AppLoadingVariant.button, + size: 18, + trackColor: AppColors.blue200, + ) + : Text( + '保存', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: enabled ? AppColors.blue600 : AppColors.slate400, + ), + ), + ), + ); + }, + ), + ); + } + Widget _buildHeader() { return Container( height: 56, @@ -473,7 +528,7 @@ class _CreateEventSheetState extends State context: context, backgroundColor: Colors.transparent, isScrollControlled: true, - builder: (context) => _DateTimePickerSheet( + builder: (context) => DateTimePickerSheet( initialDate: date, initialTime: time, minTime: minTime, @@ -587,10 +642,7 @@ class _CreateEventSheetState extends State } int? _sanitizeReminderMinutes(int? minutes) { - if (minutes == null || minutes < 0) { - return null; - } - return minutes; + return (minutes != null && minutes >= 0) ? minutes : null; } List _buildReminderOptions() { @@ -692,368 +744,3 @@ class _CreateEventSheetState extends State } } } - -class _DateTimePickerSheet extends StatefulWidget { - final DateTime initialDate; - final DateTime initialTime; - final DateTime? minTime; - - const _DateTimePickerSheet({ - required this.initialDate, - required this.initialTime, - this.minTime, - }); - - @override - State<_DateTimePickerSheet> createState() => _DateTimePickerSheetState(); -} - -class _DateTimePickerSheetState extends State<_DateTimePickerSheet> { - late int _selectedYear; - late int _selectedMonth; - late int _selectedDay; - late int _selectedHour; - late int _selectedMinute; - - late FixedExtentScrollController _yearController; - late FixedExtentScrollController _monthController; - late FixedExtentScrollController _dayController; - late FixedExtentScrollController _hourController; - late FixedExtentScrollController _minuteController; - - static final int _baseYear = DateTime.now().year; - static final List _years = List.generate(21, (i) => _baseYear - 10 + i); - static final List _months = List.generate(12, (i) => i + 1); - static final List _allHours = List.generate(24, (i) => i); - static final List _allMinutes = List.generate(60, (i) => i); - - List _days = []; - late List _filteredHours; - late List _filteredMinutes; - - List _getFilteredHours() { - if (widget.minTime == null) return _allHours; - final minDate = widget.minTime!; - if (_selectedYear > minDate.year || - (_selectedYear == minDate.year && _selectedMonth > minDate.month) || - (_selectedYear == minDate.year && - _selectedMonth == minDate.month && - _selectedDay > minDate.day)) { - return _allHours; - } - if (_selectedYear == minDate.year && - _selectedMonth == minDate.month && - _selectedDay == minDate.day) { - return _allHours.where((h) => h > minDate.hour).toList(); - } - return _allHours; - } - - List _getFilteredMinutes() { - if (widget.minTime == null) return _allMinutes; - final minDate = widget.minTime!; - if (_selectedYear > minDate.year || - (_selectedYear == minDate.year && _selectedMonth > minDate.month) || - (_selectedYear == minDate.year && - _selectedMonth == minDate.month && - _selectedDay > minDate.day)) { - return _allMinutes; - } - if (_selectedYear == minDate.year && - _selectedMonth == minDate.month && - _selectedDay == minDate.day && - _selectedHour == minDate.hour) { - return _allMinutes.where((m) => m > minDate.minute).toList(); - } - return _allMinutes; - } - - @override - void initState() { - super.initState(); - _selectedYear = widget.initialDate.year; - _selectedMonth = widget.initialDate.month; - _selectedDay = widget.initialDate.day; - _selectedHour = widget.initialTime.hour; - _selectedMinute = widget.initialTime.minute; - _filteredHours = _getFilteredHours(); - _filteredMinutes = _getFilteredMinutes(); - _updateDays(); - - _yearController = FixedExtentScrollController( - initialItem: _years.indexOf(_selectedYear), - ); - _monthController = FixedExtentScrollController( - initialItem: _selectedMonth - 1, - ); - _dayController = FixedExtentScrollController(initialItem: _selectedDay - 1); - _hourController = FixedExtentScrollController( - initialItem: _filteredHours.indexOf(_selectedHour), - ); - _minuteController = FixedExtentScrollController( - initialItem: _filteredMinutes.indexOf(_selectedMinute), - ); - } - - void _updateDays() { - _days = List.generate( - DateTime(_selectedYear, _selectedMonth + 1, 0).day, - (i) => i + 1, - ); - } - - @override - void dispose() { - _yearController.dispose(); - _monthController.dispose(); - _dayController.dispose(); - _hourController.dispose(); - _minuteController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Container( - height: 420, - decoration: const BoxDecoration( - color: AppColors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), - ), - child: Column( - children: [ - _buildHeader(), - Expanded( - child: Row( - children: [ - Expanded( - flex: 3, - child: Column( - children: [ - _buildPickerLabel('日期'), - Expanded( - child: Row( - children: [ - Expanded( - child: _buildPicker(_years, _yearController, (v) { - setState(() { - _selectedYear = v; - _updateDays(); - if (_selectedDay > _days.length) { - _selectedDay = _days.length; - _dayController.jumpToItem(_selectedDay - 1); - } - }); - }, (v) => '$v'), - ), - const Text( - '年', - style: TextStyle( - fontSize: 14, - color: AppColors.slate600, - ), - ), - Expanded( - child: _buildPicker(_months, _monthController, ( - v, - ) { - setState(() { - _selectedMonth = v; - _updateDays(); - if (_selectedDay > _days.length) { - _selectedDay = _days.length; - _dayController.jumpToItem(_selectedDay - 1); - } - }); - }, (v) => '$v'), - ), - const Text( - '月', - style: TextStyle( - fontSize: 14, - color: AppColors.slate600, - ), - ), - Expanded( - child: _buildPicker( - _days, - _dayController, - (v) => setState(() => _selectedDay = v), - (v) => '$v', - ), - ), - const Text( - '日', - style: TextStyle( - fontSize: 14, - color: AppColors.slate600, - ), - ), - ], - ), - ), - ], - ), - ), - Container(width: 1, height: 180, color: AppColors.border), - Expanded( - flex: 2, - child: Column( - children: [ - _buildPickerLabel('时间'), - Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: _buildPicker( - _filteredHours, - _hourController, - (v) { - setState(() { - _selectedHour = v; - _filteredMinutes = _getFilteredMinutes(); - if (_selectedMinute > - _filteredMinutes.last) { - _selectedMinute = - _filteredMinutes.isNotEmpty - ? _filteredMinutes.last - : 0; - _minuteController.jumpToItem( - _filteredMinutes.indexOf( - _selectedMinute, - ), - ); - } - }); - }, - (v) => v.toString().padLeft(2, '0'), - itemExtent: 50, - ), - ), - const Text( - ' : ', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: AppColors.slate600, - ), - ), - Expanded( - child: _buildPicker( - _filteredMinutes, - _minuteController, - (v) => setState(() => _selectedMinute = v), - (v) => v.toString().padLeft(2, '0'), - itemExtent: 50, - ), - ), - ], - ), - ), - ], - ), - ), - ], - ), - ), - ], - ), - ); - } - - Widget _buildHeader() { - return Container( - height: 56, - padding: const EdgeInsets.symmetric(horizontal: 16), - decoration: const BoxDecoration( - border: Border(bottom: BorderSide(color: AppColors.border)), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GestureDetector( - onTap: () => Navigator.pop(context), - child: const Text( - '取消', - style: TextStyle(fontSize: 17, color: AppColors.slate600), - ), - ), - const Text( - '选择时间', - style: TextStyle( - fontSize: 17, - fontWeight: FontWeight.w600, - color: AppColors.slate900, - ), - ), - GestureDetector( - onTap: () { - Navigator.pop(context, ( - DateTime(_selectedYear, _selectedMonth, _selectedDay), - DateTime(2000, 1, 1, _selectedHour, _selectedMinute), - )); - }, - child: const Text( - '确定', - style: TextStyle( - fontSize: 17, - fontWeight: FontWeight.w600, - color: AppColors.blue600, - ), - ), - ), - ], - ), - ); - } - - Widget _buildPickerLabel(String label) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: Text( - label, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: AppColors.slate700, - ), - ), - ); - } - - Widget _buildPicker( - List items, - FixedExtentScrollController controller, - ValueChanged onChanged, - String Function(int) formatter, { - double itemExtent = 40, - }) { - return CupertinoPicker( - scrollController: controller, - itemExtent: itemExtent, - magnification: 1.2, - squeeze: 0.8, - useMagnifier: true, - onSelectedItemChanged: (index) => onChanged(items[index]), - selectionOverlay: Container( - decoration: BoxDecoration( - border: Border.symmetric( - horizontal: BorderSide( - color: AppColors.blue100.withValues(alpha: 0.5), - width: 1, - ), - ), - ), - ), - children: List.generate(items.length, (index) { - return Center( - child: Text( - formatter(items[index]), - style: const TextStyle(fontSize: 18, color: AppColors.slate900), - ), - ); - }), - ); - } -} diff --git a/apps/lib/features/calendar/ui/widgets/date_time_picker_sheet.dart b/apps/lib/features/calendar/ui/widgets/date_time_picker_sheet.dart new file mode 100644 index 0000000..598727c --- /dev/null +++ b/apps/lib/features/calendar/ui/widgets/date_time_picker_sheet.dart @@ -0,0 +1,377 @@ +import 'package:flutter/cupertino.dart'; + +import '../../../../core/theme/design_tokens.dart'; + +class DateTimePickerSheet extends StatefulWidget { + final DateTime initialDate; + final DateTime initialTime; + final DateTime? minTime; + + const DateTimePickerSheet({ + super.key, + required this.initialDate, + required this.initialTime, + this.minTime, + }); + + @override + State createState() => _DateTimePickerSheetState(); +} + +class _DateTimePickerSheetState extends State { + late int _selectedYear; + late int _selectedMonth; + late int _selectedDay; + late int _selectedHour; + late int _selectedMinute; + + late FixedExtentScrollController _yearController; + late FixedExtentScrollController _monthController; + late FixedExtentScrollController _dayController; + late FixedExtentScrollController _hourController; + late FixedExtentScrollController _minuteController; + + static final int _baseYear = DateTime.now().year; + static final List _years = List.generate(21, (i) => _baseYear - 10 + i); + static final List _months = List.generate(12, (i) => i + 1); + static final List _allHours = List.generate(24, (i) => i); + static final List _allMinutes = List.generate(60, (i) => i); + + List _days = []; + late List _filteredHours; + late List _filteredMinutes; + + List _getFilteredHours() { + if (widget.minTime == null) return _allHours; + final minDate = widget.minTime!; + if (_selectedYear > minDate.year || + (_selectedYear == minDate.year && _selectedMonth > minDate.month) || + (_selectedYear == minDate.year && + _selectedMonth == minDate.month && + _selectedDay > minDate.day)) { + return _allHours; + } + if (_selectedYear == minDate.year && + _selectedMonth == minDate.month && + _selectedDay == minDate.day) { + return _allHours.where((h) => h > minDate.hour).toList(); + } + return _allHours; + } + + List _getFilteredMinutes() { + if (widget.minTime == null) return _allMinutes; + final minDate = widget.minTime!; + if (_selectedYear > minDate.year || + (_selectedYear == minDate.year && _selectedMonth > minDate.month) || + (_selectedYear == minDate.year && + _selectedMonth == minDate.month && + _selectedDay > minDate.day)) { + return _allMinutes; + } + if (_selectedYear == minDate.year && + _selectedMonth == minDate.month && + _selectedDay == minDate.day && + _selectedHour == minDate.hour) { + return _allMinutes.where((m) => m > minDate.minute).toList(); + } + return _allMinutes; + } + + @override + void initState() { + super.initState(); + _selectedYear = widget.initialDate.year; + _selectedMonth = widget.initialDate.month; + _selectedDay = widget.initialDate.day; + _selectedHour = widget.initialTime.hour; + _selectedMinute = widget.initialTime.minute; + _filteredHours = _getFilteredHours(); + _filteredMinutes = _getFilteredMinutes(); + _updateDays(); + + _yearController = FixedExtentScrollController( + initialItem: _years.indexOf(_selectedYear), + ); + _monthController = FixedExtentScrollController( + initialItem: _selectedMonth - 1, + ); + _dayController = FixedExtentScrollController(initialItem: _selectedDay - 1); + _hourController = FixedExtentScrollController( + initialItem: _filteredHours.indexOf(_selectedHour), + ); + _minuteController = FixedExtentScrollController( + initialItem: _filteredMinutes.indexOf(_selectedMinute), + ); + } + + void _updateDays() { + _days = List.generate( + DateTime(_selectedYear, _selectedMonth + 1, 0).day, + (i) => i + 1, + ); + } + + @override + void dispose() { + _yearController.dispose(); + _monthController.dispose(); + _dayController.dispose(); + _hourController.dispose(); + _minuteController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + height: 420, + decoration: const BoxDecoration( + color: AppColors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Column( + children: [ + _buildHeader(), + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildPickerLabel('日期'), + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: _buildPicker(_years, _yearController, (v) { + setState(() { + _selectedYear = v; + _updateDays(); + if (_selectedDay > _days.length) { + _selectedDay = _days.length; + _dayController.jumpToItem(_selectedDay - 1); + } + }); + }, (v) => '$v'), + ), + const Text( + '年', + style: TextStyle( + fontSize: 14, + color: AppColors.slate600, + ), + ), + Expanded( + child: _buildPicker(_months, _monthController, ( + v, + ) { + setState(() { + _selectedMonth = v; + _updateDays(); + if (_selectedDay > _days.length) { + _selectedDay = _days.length; + _dayController.jumpToItem(_selectedDay - 1); + } + }); + }, (v) => '$v'), + ), + const Text( + '月', + style: TextStyle( + fontSize: 14, + color: AppColors.slate600, + ), + ), + Expanded( + child: _buildPicker( + _days, + _dayController, + (v) => setState(() => _selectedDay = v), + (v) => '$v', + ), + ), + const Text( + '日', + style: TextStyle( + fontSize: 14, + color: AppColors.slate600, + ), + ), + ], + ), + ), + ], + ), + ), + Container(width: 1, height: 180, color: AppColors.border), + Expanded( + flex: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildPickerLabel('时间'), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: _buildPicker( + _filteredHours, + _hourController, + (v) { + setState(() { + _selectedHour = v; + _filteredMinutes = _getFilteredMinutes(); + if (_filteredMinutes.isEmpty) { + _selectedMinute = 0; + return; + } + if (_selectedMinute > + _filteredMinutes.last) { + _selectedMinute = _filteredMinutes.last; + _minuteController.jumpToItem( + _filteredMinutes.indexOf( + _selectedMinute, + ), + ); + } + }); + }, + (v) => v.toString().padLeft(2, '0'), + itemExtent: 50, + ), + ), + const Text( + ' : ', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: AppColors.slate600, + ), + ), + Expanded( + child: _buildPicker( + _filteredMinutes, + _minuteController, + (v) => setState(() => _selectedMinute = v), + (v) => v.toString().padLeft(2, '0'), + itemExtent: 50, + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildHeader() { + return Container( + height: 56, + padding: const EdgeInsets.symmetric(horizontal: 16), + decoration: const BoxDecoration( + border: Border(bottom: BorderSide(color: AppColors.border)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + GestureDetector( + onTap: () => Navigator.pop(context), + child: const Text( + '取消', + style: TextStyle(fontSize: 17, color: AppColors.slate600), + ), + ), + const Text( + '选择时间', + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.w600, + color: AppColors.slate900, + ), + ), + GestureDetector( + onTap: () { + Navigator.pop(context, ( + DateTime(_selectedYear, _selectedMonth, _selectedDay), + DateTime(2000, 1, 1, _selectedHour, _selectedMinute), + )); + }, + child: const Text( + '确定', + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.w600, + color: AppColors.blue600, + ), + ), + ), + ], + ), + ); + } + + Widget _buildPickerLabel(String label) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Text( + label, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: AppColors.slate700, + ), + textAlign: TextAlign.center, + ), + ); + } + + Widget _buildPicker( + List items, + FixedExtentScrollController controller, + ValueChanged onChanged, + String Function(int) formatter, { + double itemExtent = 40, + }) { + return CupertinoPicker( + scrollController: controller, + itemExtent: itemExtent, + magnification: 1.2, + squeeze: 0.8, + useMagnifier: true, + onSelectedItemChanged: (index) => onChanged(items[index]), + selectionOverlay: Container( + decoration: BoxDecoration( + border: Border.symmetric( + horizontal: BorderSide( + color: AppColors.blue100.withValues(alpha: 0.5), + width: 1, + ), + ), + ), + ), + children: List.generate(items.length, (index) { + return Center( + child: Text( + formatter(items[index]), + style: const TextStyle(fontSize: 18, color: AppColors.slate900), + ), + ); + }), + ); + } +}