feat(apps/calendar): 新增日历事件创建/编辑/分享功能
This commit is contained in:
@@ -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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user