feat(apps/calendar): 新增日历事件创建/编辑/分享功能
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import '../../../../core/router/app_routes.dart';
|
||||
import '../../../home/ui/navigation/home_return_policy.dart';
|
||||
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
@@ -14,7 +16,6 @@ import '../dayweek/day_event_layout_engine.dart';
|
||||
import '../dayweek/day_timeline_metrics.dart';
|
||||
import '../dayweek/day_view_scale.dart';
|
||||
import '../widgets/bottom_dock.dart';
|
||||
import '../widgets/create_event_sheet.dart';
|
||||
|
||||
class CalendarDayWeekScreen extends StatefulWidget {
|
||||
final DateTime? initialDate;
|
||||
@@ -118,7 +119,7 @@ class _CalendarDayWeekScreenState extends State<CalendarDayWeekScreen>
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, result) {
|
||||
if (!didPop) {
|
||||
context.go('/home');
|
||||
returnToHomePreserveState(context);
|
||||
}
|
||||
},
|
||||
child: SafeArea(
|
||||
@@ -313,10 +314,8 @@ class _CalendarDayWeekScreenState extends State<CalendarDayWeekScreen>
|
||||
if (isNotToday) const SizedBox(width: 8),
|
||||
AppPressable(
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
onTap: () => CreateEventSheet.show(
|
||||
context,
|
||||
initialDate: _selectedDate,
|
||||
onSaved: _loadEvents,
|
||||
onTap: () => context.push(
|
||||
'${AppRoutes.calendarEventCreate}?date=${formatYmd(_selectedDate)}',
|
||||
),
|
||||
child: Container(
|
||||
width: 36,
|
||||
@@ -636,7 +635,8 @@ class _CalendarDayWeekScreenState extends State<CalendarDayWeekScreen>
|
||||
height: tapHeight,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => context.push('/calendar/events/${layout.event.id}'),
|
||||
onTap: () =>
|
||||
context.push(AppRoutes.calendarEventDetail(layout.event.id)),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
@@ -696,13 +696,13 @@ class _CalendarDayWeekScreenState extends State<CalendarDayWeekScreen>
|
||||
activeTab: DockTab.calendar,
|
||||
onTodoTap: () {
|
||||
_calendarManager.setViewType(CalendarViewType.day);
|
||||
context.go('/todo');
|
||||
context.push(AppRoutes.todoList);
|
||||
},
|
||||
onCalendarTap: () {
|
||||
_calendarManager.setViewType(CalendarViewType.day);
|
||||
context.go('/calendar/month');
|
||||
context.push(AppRoutes.calendarMonth);
|
||||
},
|
||||
onHomeTap: () => context.go('/home'),
|
||||
onHomeTap: () => returnToHomePreserveState(context),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:go_router/go_router.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../core/router/app_routes.dart';
|
||||
import '../../../../core/notifications/local_notification_service.dart';
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../../shared/widgets/app_loading_indicator.dart';
|
||||
@@ -11,8 +12,6 @@ import '../../../../shared/widgets/destructive_action_sheet.dart';
|
||||
import '../../data/services/calendar_service.dart';
|
||||
import '../../data/models/schedule_item_model.dart';
|
||||
import '../utils/event_color_resolver.dart';
|
||||
import '../widgets/create_event_sheet.dart';
|
||||
import '../widgets/calendar_share_dialog.dart';
|
||||
|
||||
enum _CalendarHeaderAction { edit, delete, share }
|
||||
|
||||
@@ -58,34 +57,50 @@ class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
|
||||
}
|
||||
if (_event == null) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF8FAFC),
|
||||
backgroundColor: AppColors.background,
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [AppColors.homeBackgroundTop, AppColors.background],
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Text(
|
||||
'Event not found',
|
||||
style: TextStyle(color: AppColors.slate600),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
height: AppSpacing.xxl * 2,
|
||||
child: TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
style: TextButton.styleFrom(
|
||||
const BackTitlePageHeader(title: '日程详情'),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.lg,
|
||||
horizontal: AppSpacing.xl,
|
||||
),
|
||||
backgroundColor: AppColors.blue600,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
'未找到该日程',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
const Text(
|
||||
'可能已被删除,或你没有访问权限。',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppColors.slate500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'返回',
|
||||
style: TextStyle(color: AppColors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -97,15 +112,41 @@ class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
|
||||
|
||||
final event = _event!;
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF8FAFC),
|
||||
backgroundColor: AppColors.background,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildHeader(event),
|
||||
Expanded(child: _buildDetailOverlay(event)),
|
||||
_buildInputContainer(),
|
||||
],
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [AppColors.homeBackgroundTop, AppColors.background],
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildHeader(event),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
AppSpacing.lg,
|
||||
AppSpacing.sm,
|
||||
AppSpacing.lg,
|
||||
AppSpacing.xl,
|
||||
),
|
||||
children: [
|
||||
_buildHeroSurface(event),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_buildMetaSurface(event),
|
||||
if (_hasExtraInfo(event)) ...[
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_buildExtraSurface(event),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -123,7 +164,7 @@ class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
|
||||
final items = <DetailHeaderActionItem<_CalendarHeaderAction>>[];
|
||||
if (event.canEdit) {
|
||||
items.add(
|
||||
const DetailHeaderActionItem<_CalendarHeaderAction>(
|
||||
DetailHeaderActionItem<_CalendarHeaderAction>(
|
||||
value: _CalendarHeaderAction.edit,
|
||||
label: '编辑',
|
||||
icon: LucideIcons.pencil,
|
||||
@@ -132,7 +173,7 @@ class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
|
||||
}
|
||||
if (event.canDelete) {
|
||||
items.add(
|
||||
const DetailHeaderActionItem<_CalendarHeaderAction>(
|
||||
DetailHeaderActionItem<_CalendarHeaderAction>(
|
||||
value: _CalendarHeaderAction.delete,
|
||||
label: '删除',
|
||||
icon: LucideIcons.trash2,
|
||||
@@ -142,7 +183,7 @@ class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
|
||||
}
|
||||
if (event.canInvite) {
|
||||
items.add(
|
||||
const DetailHeaderActionItem<_CalendarHeaderAction>(
|
||||
DetailHeaderActionItem<_CalendarHeaderAction>(
|
||||
value: _CalendarHeaderAction.share,
|
||||
label: '分享',
|
||||
icon: LucideIcons.share2,
|
||||
@@ -162,85 +203,220 @@ class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
|
||||
) {
|
||||
switch (action) {
|
||||
case _CalendarHeaderAction.edit:
|
||||
CreateEventSheet.edit(
|
||||
context,
|
||||
event,
|
||||
onSaved: () {
|
||||
setState(() {
|
||||
_loadEvent();
|
||||
});
|
||||
},
|
||||
);
|
||||
context.push(AppRoutes.calendarEventEdit(event.id)).then((_) {
|
||||
_loadEvent();
|
||||
});
|
||||
return;
|
||||
case _CalendarHeaderAction.delete:
|
||||
_showDeleteConfirmation();
|
||||
return;
|
||||
case _CalendarHeaderAction.share:
|
||||
CalendarShareDialog.show(
|
||||
context,
|
||||
event.id,
|
||||
event.title,
|
||||
canInvite: event.canInvite,
|
||||
canEdit: event.canEdit,
|
||||
);
|
||||
context.push(AppRoutes.calendarEventShare(event.id));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildDetailOverlay(ScheduleItemModel event) {
|
||||
final startAt = event.startAt;
|
||||
final endAt = event.endAt;
|
||||
final dateStr =
|
||||
'${startAt.year}年${startAt.month}月${startAt.day}日 ${_getWeekday(startAt.weekday)}';
|
||||
final timeStr = endAt != null
|
||||
? '${_formatTime(startAt)} - ${_formatTime(endAt)}'
|
||||
: _formatTime(startAt);
|
||||
Widget _buildHeroSurface(ScheduleItemModel event) {
|
||||
final color = resolveEventColor(
|
||||
status: event.status,
|
||||
colorHex: event.metadata?.color,
|
||||
);
|
||||
final timeRange = _formatRangeLabel(event.startAt, event.endAt);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, top: 8, bottom: 12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(color: const Color(0xFFD8E3F5)),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: SizedBox(
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
border: Border.all(color: AppColors.borderTertiary),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.slate200.withValues(alpha: 0.38),
|
||||
blurRadius: AppRadius.lg,
|
||||
offset: const Offset(0, AppSpacing.xs),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: AppSpacing.sm,
|
||||
height: AppSpacing.xxl,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.md),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
event.title,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.slate900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
_buildStatusBadge(event.status),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surfaceInfoLight,
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
border: Border.all(color: AppColors.borderQuaternary),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildTitleRow(event),
|
||||
const SizedBox(height: 14),
|
||||
Container(height: 1, color: const Color(0xFFE5E7EB)),
|
||||
const SizedBox(height: 14),
|
||||
_buildDetailField('日期', dateStr),
|
||||
const SizedBox(height: 14),
|
||||
_buildDetailField('时间范围', timeStr),
|
||||
const SizedBox(height: 14),
|
||||
_buildDetailField(
|
||||
'提醒时间',
|
||||
_formatReminderText(event.metadata?.reminderMinutes),
|
||||
const Text(
|
||||
'时间安排',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
timeRange,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.slate800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
_buildColorField(event),
|
||||
const SizedBox(height: 14),
|
||||
if (event.metadata?.location != null) ...[
|
||||
_buildDetailField('地点', event.metadata!.location!),
|
||||
const SizedBox(height: 14),
|
||||
],
|
||||
if (event.description != null) ...[
|
||||
_buildDetailField('描述', event.description!),
|
||||
const SizedBox(height: 14),
|
||||
],
|
||||
if (event.metadata?.notes != null) ...[
|
||||
_buildNotesField(event.metadata!.notes!),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMetaSurface(ScheduleItemModel event) {
|
||||
final startAt = event.startAt;
|
||||
final dateStr =
|
||||
'${startAt.year}年${startAt.month}月${startAt.day}日 ${_getWeekday(startAt.weekday)}';
|
||||
final color = resolveEventColor(
|
||||
status: event.status,
|
||||
colorHex: event.metadata?.color,
|
||||
);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
border: Border.all(color: AppColors.borderSecondary),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'基础信息',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_buildDetailRow('日期', dateStr),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_buildDetailRow(
|
||||
'提醒',
|
||||
_formatReminderText(event.metadata?.reminderMinutes),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: AppSpacing.xxl * 3,
|
||||
child: Text(
|
||||
'颜色',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate500,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: AppSpacing.xl,
|
||||
height: AppSpacing.xl,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: AppColors.borderSecondary),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool _hasExtraInfo(ScheduleItemModel event) {
|
||||
return (event.metadata?.location?.trim().isNotEmpty ?? false) ||
|
||||
(event.description?.trim().isNotEmpty ?? false) ||
|
||||
(event.metadata?.notes?.trim().isNotEmpty ?? false);
|
||||
}
|
||||
|
||||
Widget _buildExtraSurface(ScheduleItemModel event) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surfaceSecondary,
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
border: Border.all(color: AppColors.borderSecondary),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'补充信息',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate500,
|
||||
),
|
||||
),
|
||||
if (event.metadata?.location?.trim().isNotEmpty ?? false) ...[
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_buildDetailRow('地点', event.metadata!.location!.trim()),
|
||||
],
|
||||
if (event.description?.trim().isNotEmpty ?? false) ...[
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_buildDetailRow('描述', event.description!.trim()),
|
||||
],
|
||||
if (event.metadata?.notes?.trim().isNotEmpty ?? false) ...[
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_buildDetailRow(
|
||||
'备注',
|
||||
event.metadata!.notes!.trim(),
|
||||
multiline: true,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -264,44 +440,6 @@ class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
|
||||
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 {
|
||||
final confirmed = await showDestructiveActionSheet(
|
||||
context,
|
||||
@@ -322,140 +460,76 @@ class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
|
||||
context.pop();
|
||||
}
|
||||
|
||||
Widget _buildDetailField(String label, String value) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate400,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate900,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
String _formatRangeLabel(DateTime startAt, DateTime? endAt) {
|
||||
final dateLabel =
|
||||
'${startAt.month}月${startAt.day}日 ${_getWeekday(startAt.weekday)}';
|
||||
if (endAt == null) {
|
||||
return '$dateLabel ${_formatTime(startAt)}';
|
||||
}
|
||||
return '$dateLabel ${_formatTime(startAt)} - ${_formatTime(endAt)}';
|
||||
}
|
||||
|
||||
Widget _buildColorField(ScheduleItemModel event) {
|
||||
final color = resolveEventColor(
|
||||
status: event.status,
|
||||
colorHex: event.metadata?.color,
|
||||
);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'日程颜色',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate400,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 28,
|
||||
height: 28,
|
||||
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNotesField(String notes) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'备注',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate400,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFDFEFF),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0xFFDCE5F4)),
|
||||
),
|
||||
child: Text(
|
||||
notes,
|
||||
style: const TextStyle(fontSize: 14, color: AppColors.slate700),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInputContainer() {
|
||||
Widget _buildStatusBadge(ScheduleStatus status) {
|
||||
final isArchived = status == ScheduleStatus.archived;
|
||||
return Container(
|
||||
height: 80,
|
||||
padding: const EdgeInsets.all(16),
|
||||
color: const Color(0xFFF8FAFC),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
border: Border.all(color: const Color(0xFFDCE5F4)),
|
||||
),
|
||||
child: const Icon(
|
||||
LucideIcons.plus,
|
||||
size: 20,
|
||||
color: AppColors.slate500,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 48,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'输入消息...',
|
||||
style: TextStyle(fontSize: 14, color: AppColors.slate400),
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
LucideIcons.mic,
|
||||
size: 20,
|
||||
color: AppColors.slate500,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.sm,
|
||||
vertical: AppSpacing.xs,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isArchived
|
||||
? AppColors.feedbackWarningSurface
|
||||
: AppColors.feedbackSuccessSurface,
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
border: Border.all(
|
||||
color: isArchived
|
||||
? AppColors.feedbackWarningBorder
|
||||
: AppColors.feedbackSuccessBorder,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
isArchived ? '已过期' : '启用',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: isArchived
|
||||
? AppColors.feedbackWarningText
|
||||
: AppColors.feedbackSuccessText,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailRow(String label, String value, {bool multiline = false}) {
|
||||
return Row(
|
||||
crossAxisAlignment: multiline
|
||||
? CrossAxisAlignment.start
|
||||
: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: AppSpacing.xxl * 3,
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate500,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
height: multiline ? 1.4 : 1.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate800,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:lucide_icons/lucide_icons.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../core/router/app_routes.dart';
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../../shared/widgets/app_pressable.dart';
|
||||
import '../../../home/ui/navigation/home_return_policy.dart';
|
||||
import '../calendar_state_manager.dart';
|
||||
import '../calendar_time_utils.dart';
|
||||
import '../utils/event_color_resolver.dart';
|
||||
import '../widgets/bottom_dock.dart';
|
||||
import '../widgets/create_event_sheet.dart';
|
||||
import '../../data/models/schedule_item_model.dart';
|
||||
import '../../data/services/calendar_service.dart';
|
||||
|
||||
@@ -104,7 +105,7 @@ class _CalendarMonthScreenState extends State<CalendarMonthScreen>
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, result) {
|
||||
if (!didPop) {
|
||||
context.go('/home');
|
||||
returnToHomePreserveState(context);
|
||||
}
|
||||
},
|
||||
child: SafeArea(
|
||||
@@ -171,10 +172,7 @@ class _CalendarMonthScreenState extends State<CalendarMonthScreen>
|
||||
const Spacer(),
|
||||
AppPressable(
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
onTap: () => CreateEventSheet.show(
|
||||
context,
|
||||
onSaved: _loadMonthEvents,
|
||||
),
|
||||
onTap: () => context.push(AppRoutes.calendarEventCreate),
|
||||
child: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
@@ -293,7 +291,9 @@ class _CalendarMonthScreenState extends State<CalendarMonthScreen>
|
||||
});
|
||||
_calendarManager.setSelectedDate(date);
|
||||
_calendarManager.setViewType(CalendarViewType.month);
|
||||
context.push('/calendar/dayweek?date=${formatYmd(date)}');
|
||||
context.push(
|
||||
'${AppRoutes.calendarDayWeek}?date=${formatYmd(date)}',
|
||||
);
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 140),
|
||||
@@ -400,7 +400,9 @@ class _CalendarMonthScreenState extends State<CalendarMonthScreen>
|
||||
onTap: () {
|
||||
_calendarManager.setSelectedDate(date);
|
||||
_calendarManager.setViewType(CalendarViewType.day);
|
||||
context.push('/calendar/dayweek?date=${formatYmd(date)}');
|
||||
context.push(
|
||||
'${AppRoutes.calendarDayWeek}?date=${formatYmd(date)}',
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
'+$remainingCount',
|
||||
@@ -517,10 +519,10 @@ class _CalendarMonthScreenState extends State<CalendarMonthScreen>
|
||||
activeTab: DockTab.calendar,
|
||||
onTodoTap: () {
|
||||
_calendarManager.setViewType(CalendarViewType.month);
|
||||
context.go('/todo');
|
||||
context.push(AppRoutes.todoList);
|
||||
},
|
||||
onCalendarTap: () {},
|
||||
onHomeTap: () => context.go('/home'),
|
||||
onHomeTap: () => returnToHomePreserveState(context),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user