feat(apps/calendar): 新增日历事件创建/编辑/分享功能

This commit is contained in:
zl-q
2026-03-19 00:51:51 +08:00
parent adccecd691
commit 14ccf2cb28
8 changed files with 959 additions and 654 deletions
@@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:lucide_icons/lucide_icons.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/di/injection.dart';
import '../../../../core/theme/design_tokens.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_timeline_metrics.dart';
import '../dayweek/day_view_scale.dart'; import '../dayweek/day_view_scale.dart';
import '../widgets/bottom_dock.dart'; import '../widgets/bottom_dock.dart';
import '../widgets/create_event_sheet.dart';
class CalendarDayWeekScreen extends StatefulWidget { class CalendarDayWeekScreen extends StatefulWidget {
final DateTime? initialDate; final DateTime? initialDate;
@@ -118,7 +119,7 @@ class _CalendarDayWeekScreenState extends State<CalendarDayWeekScreen>
canPop: false, canPop: false,
onPopInvokedWithResult: (didPop, result) { onPopInvokedWithResult: (didPop, result) {
if (!didPop) { if (!didPop) {
context.go('/home'); returnToHomePreserveState(context);
} }
}, },
child: SafeArea( child: SafeArea(
@@ -313,10 +314,8 @@ class _CalendarDayWeekScreenState extends State<CalendarDayWeekScreen>
if (isNotToday) const SizedBox(width: 8), if (isNotToday) const SizedBox(width: 8),
AppPressable( AppPressable(
borderRadius: BorderRadius.circular(AppRadius.full), borderRadius: BorderRadius.circular(AppRadius.full),
onTap: () => CreateEventSheet.show( onTap: () => context.push(
context, '${AppRoutes.calendarEventCreate}?date=${formatYmd(_selectedDate)}',
initialDate: _selectedDate,
onSaved: _loadEvents,
), ),
child: Container( child: Container(
width: 36, width: 36,
@@ -636,7 +635,8 @@ class _CalendarDayWeekScreenState extends State<CalendarDayWeekScreen>
height: tapHeight, height: tapHeight,
child: GestureDetector( child: GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onTap: () => context.push('/calendar/events/${layout.event.id}'), onTap: () =>
context.push(AppRoutes.calendarEventDetail(layout.event.id)),
child: Stack( child: Stack(
children: [ children: [
Positioned( Positioned(
@@ -696,13 +696,13 @@ class _CalendarDayWeekScreenState extends State<CalendarDayWeekScreen>
activeTab: DockTab.calendar, activeTab: DockTab.calendar,
onTodoTap: () { onTodoTap: () {
_calendarManager.setViewType(CalendarViewType.day); _calendarManager.setViewType(CalendarViewType.day);
context.go('/todo'); context.push(AppRoutes.todoList);
}, },
onCalendarTap: () { onCalendarTap: () {
_calendarManager.setViewType(CalendarViewType.day); _calendarManager.setViewType(CalendarViewType.day);
context.go('/calendar/month'); context.push(AppRoutes.calendarMonth);
}, },
onHomeTap: () => context.go('/home'), onHomeTap: () => returnToHomePreserveState(context),
); );
} }
} }
@@ -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),
),
);
}
}
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart'; import 'package:lucide_icons/lucide_icons.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import '../../../../core/di/injection.dart'; import '../../../../core/di/injection.dart';
import '../../../../core/router/app_routes.dart';
import '../../../../core/notifications/local_notification_service.dart'; import '../../../../core/notifications/local_notification_service.dart';
import '../../../../core/theme/design_tokens.dart'; import '../../../../core/theme/design_tokens.dart';
import '../../../../shared/widgets/app_loading_indicator.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/services/calendar_service.dart';
import '../../data/models/schedule_item_model.dart'; import '../../data/models/schedule_item_model.dart';
import '../utils/event_color_resolver.dart'; import '../utils/event_color_resolver.dart';
import '../widgets/create_event_sheet.dart';
import '../widgets/calendar_share_dialog.dart';
enum _CalendarHeaderAction { edit, delete, share } enum _CalendarHeaderAction { edit, delete, share }
@@ -58,34 +57,50 @@ class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
} }
if (_event == null) { if (_event == null) {
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF8FAFC), backgroundColor: AppColors.background,
body: SafeArea( body: SafeArea(
child: Center( child: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [AppColors.homeBackgroundTop, AppColors.background],
),
),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
const Text( const BackTitlePageHeader(title: '日程详情'),
'Event not found', Expanded(
style: TextStyle(color: AppColors.slate600), child: Center(
), child: Padding(
const SizedBox(height: 16),
SizedBox(
height: AppSpacing.xxl * 2,
child: TextButton(
onPressed: () => context.pop(),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.lg, horizontal: AppSpacing.xl,
), ),
backgroundColor: AppColors.blue600, child: Column(
shape: RoundedRectangleBorder( mainAxisSize: MainAxisSize.min,
borderRadius: BorderRadius.circular(AppRadius.full), 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<CalendarEventDetailScreen> {
final event = _event!; final event = _event!;
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF8FAFC), backgroundColor: AppColors.background,
body: SafeArea( body: SafeArea(
child: Column( child: Container(
crossAxisAlignment: CrossAxisAlignment.stretch, decoration: const BoxDecoration(
children: [ gradient: LinearGradient(
_buildHeader(event), begin: Alignment.topCenter,
Expanded(child: _buildDetailOverlay(event)), end: Alignment.bottomCenter,
_buildInputContainer(), 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<CalendarEventDetailScreen> {
final items = <DetailHeaderActionItem<_CalendarHeaderAction>>[]; final items = <DetailHeaderActionItem<_CalendarHeaderAction>>[];
if (event.canEdit) { if (event.canEdit) {
items.add( items.add(
const DetailHeaderActionItem<_CalendarHeaderAction>( DetailHeaderActionItem<_CalendarHeaderAction>(
value: _CalendarHeaderAction.edit, value: _CalendarHeaderAction.edit,
label: '编辑', label: '编辑',
icon: LucideIcons.pencil, icon: LucideIcons.pencil,
@@ -132,7 +173,7 @@ class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
} }
if (event.canDelete) { if (event.canDelete) {
items.add( items.add(
const DetailHeaderActionItem<_CalendarHeaderAction>( DetailHeaderActionItem<_CalendarHeaderAction>(
value: _CalendarHeaderAction.delete, value: _CalendarHeaderAction.delete,
label: '删除', label: '删除',
icon: LucideIcons.trash2, icon: LucideIcons.trash2,
@@ -142,7 +183,7 @@ class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
} }
if (event.canInvite) { if (event.canInvite) {
items.add( items.add(
const DetailHeaderActionItem<_CalendarHeaderAction>( DetailHeaderActionItem<_CalendarHeaderAction>(
value: _CalendarHeaderAction.share, value: _CalendarHeaderAction.share,
label: '分享', label: '分享',
icon: LucideIcons.share2, icon: LucideIcons.share2,
@@ -162,85 +203,220 @@ class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
) { ) {
switch (action) { switch (action) {
case _CalendarHeaderAction.edit: case _CalendarHeaderAction.edit:
CreateEventSheet.edit( context.push(AppRoutes.calendarEventEdit(event.id)).then((_) {
context, _loadEvent();
event, });
onSaved: () {
setState(() {
_loadEvent();
});
},
);
return; return;
case _CalendarHeaderAction.delete: case _CalendarHeaderAction.delete:
_showDeleteConfirmation(); _showDeleteConfirmation();
return; return;
case _CalendarHeaderAction.share: case _CalendarHeaderAction.share:
CalendarShareDialog.show( context.push(AppRoutes.calendarEventShare(event.id));
context,
event.id,
event.title,
canInvite: event.canInvite,
canEdit: event.canEdit,
);
return; return;
} }
} }
Widget _buildDetailOverlay(ScheduleItemModel event) { Widget _buildHeroSurface(ScheduleItemModel event) {
final startAt = event.startAt; final color = resolveEventColor(
final endAt = event.endAt; status: event.status,
final dateStr = colorHex: event.metadata?.color,
'${startAt.year}${startAt.month}${startAt.day}${_getWeekday(startAt.weekday)}'; );
final timeStr = endAt != null final timeRange = _formatRangeLabel(event.startAt, event.endAt);
? '${_formatTime(startAt)} - ${_formatTime(endAt)}'
: _formatTime(startAt);
return Padding( return Container(
padding: const EdgeInsets.only(left: 16, right: 16, top: 8, bottom: 12), padding: const EdgeInsets.all(AppSpacing.lg),
child: Container( decoration: BoxDecoration(
padding: const EdgeInsets.all(16), color: AppColors.white,
decoration: BoxDecoration( borderRadius: BorderRadius.circular(AppRadius.xl),
color: Colors.white, border: Border.all(color: AppColors.borderTertiary),
borderRadius: BorderRadius.circular(24), boxShadow: [
border: Border.all(color: const Color(0xFFD8E3F5)), BoxShadow(
), color: AppColors.slate200.withValues(alpha: 0.38),
child: SingleChildScrollView( blurRadius: AppRadius.lg,
child: SizedBox( 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, 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( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildTitleRow(event), const Text(
const SizedBox(height: 14), '时间安排',
Container(height: 1, color: const Color(0xFFE5E7EB)), style: TextStyle(
const SizedBox(height: 14), fontSize: 12,
_buildDetailField('日期', dateStr), fontWeight: FontWeight.w600,
const SizedBox(height: 14), color: AppColors.slate500,
_buildDetailField('时间范围', timeStr), ),
const SizedBox(height: 14), ),
_buildDetailField( const SizedBox(height: AppSpacing.xs),
'提醒时间', Text(
_formatReminderText(event.metadata?.reminderMinutes), 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<CalendarEventDetailScreen> {
return '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}'; 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<void> _showDeleteConfirmation() async { Future<void> _showDeleteConfirmation() async {
final confirmed = await showDestructiveActionSheet( final confirmed = await showDestructiveActionSheet(
context, context,
@@ -322,140 +460,76 @@ class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
context.pop(); context.pop();
} }
Widget _buildDetailField(String label, String value) { String _formatRangeLabel(DateTime startAt, DateTime? endAt) {
return Column( final dateLabel =
crossAxisAlignment: CrossAxisAlignment.start, '${startAt.month}${startAt.day}${_getWeekday(startAt.weekday)}';
children: [ if (endAt == null) {
Text( return '$dateLabel ${_formatTime(startAt)}';
label, }
style: const TextStyle( return '$dateLabel ${_formatTime(startAt)} - ${_formatTime(endAt)}';
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,
),
),
],
);
} }
Widget _buildColorField(ScheduleItemModel event) { Widget _buildStatusBadge(ScheduleStatus status) {
final color = resolveEventColor( final isArchived = status == ScheduleStatus.archived;
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() {
return Container( return Container(
height: 80, padding: const EdgeInsets.symmetric(
padding: const EdgeInsets.all(16), horizontal: AppSpacing.sm,
color: const Color(0xFFF8FAFC), vertical: AppSpacing.xs,
child: Row( ),
children: [ decoration: BoxDecoration(
Container( color: isArchived
width: 36, ? AppColors.feedbackWarningSurface
height: 36, : AppColors.feedbackSuccessSurface,
decoration: BoxDecoration( borderRadius: BorderRadius.circular(AppRadius.full),
color: Colors.white, border: Border.all(
borderRadius: BorderRadius.circular(18), color: isArchived
border: Border.all(color: const Color(0xFFDCE5F4)), ? AppColors.feedbackWarningBorder
), : AppColors.feedbackSuccessBorder,
child: const Icon( ),
LucideIcons.plus, ),
size: 20, child: Text(
color: AppColors.slate500, isArchived ? '已过期' : '启用',
), style: TextStyle(
), fontSize: 12,
const SizedBox(width: 8), fontWeight: FontWeight.w700,
Expanded( color: isArchived
child: Container( ? AppColors.feedbackWarningText
height: 48, : AppColors.feedbackSuccessText,
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,
),
],
),
),
),
],
), ),
); );
} }
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,
),
),
),
],
);
}
} }
@@ -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<CalendarEventEditScreen> createState() =>
_CalendarEventEditScreenState();
}
class _CalendarEventEditScreenState extends State<CalendarEventEditScreen> {
ScheduleItemModel? _event;
bool _loading = true;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
try {
_event = await sl<CalendarService>().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),
),
);
}
}
@@ -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<CalendarEventShareScreen> createState() =>
_CalendarEventShareScreenState();
}
class _CalendarEventShareScreenState extends State<CalendarEventShareScreen> {
ScheduleItemModel? _event;
bool _loading = true;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
try {
_event = await sl<CalendarService>().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,
),
),
],
),
),
);
}
}
@@ -3,13 +3,14 @@ import 'package:flutter/cupertino.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:lucide_icons/lucide_icons.dart'; import 'package:lucide_icons/lucide_icons.dart';
import '../../../../core/di/injection.dart'; import '../../../../core/di/injection.dart';
import '../../../../core/router/app_routes.dart';
import '../../../../core/theme/design_tokens.dart'; import '../../../../core/theme/design_tokens.dart';
import '../../../../shared/widgets/app_pressable.dart'; import '../../../../shared/widgets/app_pressable.dart';
import '../../../home/ui/navigation/home_return_policy.dart';
import '../calendar_state_manager.dart'; import '../calendar_state_manager.dart';
import '../calendar_time_utils.dart'; import '../calendar_time_utils.dart';
import '../utils/event_color_resolver.dart'; import '../utils/event_color_resolver.dart';
import '../widgets/bottom_dock.dart'; import '../widgets/bottom_dock.dart';
import '../widgets/create_event_sheet.dart';
import '../../data/models/schedule_item_model.dart'; import '../../data/models/schedule_item_model.dart';
import '../../data/services/calendar_service.dart'; import '../../data/services/calendar_service.dart';
@@ -104,7 +105,7 @@ class _CalendarMonthScreenState extends State<CalendarMonthScreen>
canPop: false, canPop: false,
onPopInvokedWithResult: (didPop, result) { onPopInvokedWithResult: (didPop, result) {
if (!didPop) { if (!didPop) {
context.go('/home'); returnToHomePreserveState(context);
} }
}, },
child: SafeArea( child: SafeArea(
@@ -171,10 +172,7 @@ class _CalendarMonthScreenState extends State<CalendarMonthScreen>
const Spacer(), const Spacer(),
AppPressable( AppPressable(
borderRadius: BorderRadius.circular(AppRadius.full), borderRadius: BorderRadius.circular(AppRadius.full),
onTap: () => CreateEventSheet.show( onTap: () => context.push(AppRoutes.calendarEventCreate),
context,
onSaved: _loadMonthEvents,
),
child: Container( child: Container(
width: 36, width: 36,
height: 36, height: 36,
@@ -293,7 +291,9 @@ class _CalendarMonthScreenState extends State<CalendarMonthScreen>
}); });
_calendarManager.setSelectedDate(date); _calendarManager.setSelectedDate(date);
_calendarManager.setViewType(CalendarViewType.month); _calendarManager.setViewType(CalendarViewType.month);
context.push('/calendar/dayweek?date=${formatYmd(date)}'); context.push(
'${AppRoutes.calendarDayWeek}?date=${formatYmd(date)}',
);
}, },
child: AnimatedContainer( child: AnimatedContainer(
duration: const Duration(milliseconds: 140), duration: const Duration(milliseconds: 140),
@@ -400,7 +400,9 @@ class _CalendarMonthScreenState extends State<CalendarMonthScreen>
onTap: () { onTap: () {
_calendarManager.setSelectedDate(date); _calendarManager.setSelectedDate(date);
_calendarManager.setViewType(CalendarViewType.day); _calendarManager.setViewType(CalendarViewType.day);
context.push('/calendar/dayweek?date=${formatYmd(date)}'); context.push(
'${AppRoutes.calendarDayWeek}?date=${formatYmd(date)}',
);
}, },
child: Text( child: Text(
'+$remainingCount', '+$remainingCount',
@@ -517,10 +519,10 @@ class _CalendarMonthScreenState extends State<CalendarMonthScreen>
activeTab: DockTab.calendar, activeTab: DockTab.calendar,
onTodoTap: () { onTodoTap: () {
_calendarManager.setViewType(CalendarViewType.month); _calendarManager.setViewType(CalendarViewType.month);
context.go('/todo'); context.push(AppRoutes.todoList);
}, },
onCalendarTap: () {}, onCalendarTap: () {},
onHomeTap: () => context.go('/home'), onHomeTap: () => returnToHomePreserveState(context),
); );
} }
} }
@@ -1,4 +1,3 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart'; import 'package:lucide_icons/lucide_icons.dart';
import '../../../../core/di/injection.dart'; import '../../../../core/di/injection.dart';
@@ -6,8 +5,10 @@ import '../../../../core/notifications/local_notification_service.dart';
import '../../../../core/theme/design_tokens.dart'; import '../../../../core/theme/design_tokens.dart';
import '../../../../shared/widgets/app_loading_indicator.dart'; import '../../../../shared/widgets/app_loading_indicator.dart';
import '../../../../shared/widgets/app_sheet_input_field.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.dart';
import '../../../../shared/widgets/toast/toast_type.dart'; import '../../../../shared/widgets/toast/toast_type.dart';
import 'date_time_picker_sheet.dart';
import '../../data/models/schedule_item_model.dart'; import '../../data/models/schedule_item_model.dart';
import '../../data/services/calendar_service.dart'; import '../../data/services/calendar_service.dart';
@@ -15,12 +16,14 @@ class CreateEventSheet extends StatefulWidget {
final DateTime? initialDate; final DateTime? initialDate;
final ScheduleItemModel? editingEvent; final ScheduleItemModel? editingEvent;
final VoidCallback? onSaved; final VoidCallback? onSaved;
final bool pageMode;
const CreateEventSheet({ const CreateEventSheet({
super.key, super.key,
this.initialDate, this.initialDate,
this.editingEvent, this.editingEvent,
this.onSaved, this.onSaved,
this.pageMode = false,
}); });
static Future<void> show( static Future<void> show(
@@ -135,6 +138,20 @@ class _CreateEventSheetState extends State<CreateEventSheet>
@override @override
Widget build(BuildContext context) { 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( return AnimatedPadding(
duration: const Duration(milliseconds: 150), duration: const Duration(milliseconds: 150),
curve: Curves.easeOut, curve: Curves.easeOut,
@@ -171,6 +188,44 @@ class _CreateEventSheetState extends State<CreateEventSheet>
); );
} }
Widget _buildPageHeader() {
return BackTitlePageHeader(
title: _isEditing ? '编辑日程' : '新建日程',
onBack: () => Navigator.of(context).pop(),
trailing: ValueListenableBuilder<TextEditingValue>(
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() { Widget _buildHeader() {
return Container( return Container(
height: 56, height: 56,
@@ -473,7 +528,7 @@ class _CreateEventSheetState extends State<CreateEventSheet>
context: context, context: context,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
isScrollControlled: true, isScrollControlled: true,
builder: (context) => _DateTimePickerSheet( builder: (context) => DateTimePickerSheet(
initialDate: date, initialDate: date,
initialTime: time, initialTime: time,
minTime: minTime, minTime: minTime,
@@ -587,10 +642,7 @@ class _CreateEventSheetState extends State<CreateEventSheet>
} }
int? _sanitizeReminderMinutes(int? minutes) { int? _sanitizeReminderMinutes(int? minutes) {
if (minutes == null || minutes < 0) { return (minutes != null && minutes >= 0) ? minutes : null;
return null;
}
return minutes;
} }
List<int?> _buildReminderOptions() { List<int?> _buildReminderOptions() {
@@ -692,368 +744,3 @@ class _CreateEventSheetState extends State<CreateEventSheet>
} }
} }
} }
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<int> _years = List.generate(21, (i) => _baseYear - 10 + i);
static final List<int> _months = List.generate(12, (i) => i + 1);
static final List<int> _allHours = List.generate(24, (i) => i);
static final List<int> _allMinutes = List.generate(60, (i) => i);
List<int> _days = [];
late List<int> _filteredHours;
late List<int> _filteredMinutes;
List<int> _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<int> _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<int> items,
FixedExtentScrollController controller,
ValueChanged<int> 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<Widget>.generate(items.length, (index) {
return Center(
child: Text(
formatter(items[index]),
style: const TextStyle(fontSize: 18, color: AppColors.slate900),
),
);
}),
);
}
}
@@ -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<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<int> _years = List.generate(21, (i) => _baseYear - 10 + i);
static final List<int> _months = List.generate(12, (i) => i + 1);
static final List<int> _allHours = List.generate(24, (i) => i);
static final List<int> _allMinutes = List.generate(60, (i) => i);
List<int> _days = [];
late List<int> _filteredHours;
late List<int> _filteredMinutes;
List<int> _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<int> _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<int> items,
FixedExtentScrollController controller,
ValueChanged<int> 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<Widget>.generate(items.length, (index) {
return Center(
child: Text(
formatter(items[index]),
style: const TextStyle(fontSize: 18, color: AppColors.slate900),
),
);
}),
);
}
}