diff --git a/apps/lib/features/auth/ui/screens/login_screen.dart b/apps/lib/features/auth/ui/screens/login_screen.dart index 77cd2b8..584e4d9 100644 --- a/apps/lib/features/auth/ui/screens/login_screen.dart +++ b/apps/lib/features/auth/ui/screens/login_screen.dart @@ -4,6 +4,7 @@ import 'package:formz/formz.dart'; import 'package:go_router/go_router.dart'; import '../../../../core/di/injection.dart'; +import '../../../../core/router/app_routes.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_button.dart'; import '../../../../shared/widgets/banner/app_banner.dart'; @@ -59,7 +60,7 @@ class _LoginViewState extends State { final response = await cubit.submit(); if (response != null && mounted) { context.read().add(AuthLoggedIn(user: response.user)); - context.go('/home'); + context.go(AppRoutes.homeMain); } } diff --git a/apps/lib/features/auth/ui/screens/register_verification_screen.dart b/apps/lib/features/auth/ui/screens/register_verification_screen.dart index 0034668..e2c5a8b 100644 --- a/apps/lib/features/auth/ui/screens/register_verification_screen.dart +++ b/apps/lib/features/auth/ui/screens/register_verification_screen.dart @@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:formz/formz.dart'; import 'package:go_router/go_router.dart'; +import '../../../../core/router/app_routes.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_button.dart'; import '../../../../shared/widgets/banner/app_banner.dart'; @@ -93,7 +94,7 @@ class _RegisterVerificationViewState extends State { final response = await cubit.submitStep2(); if (response != null && mounted) { context.read().add(AuthLoggedIn(user: response.user)); - context.go('/home'); + context.go(AppRoutes.homeMain); } } diff --git a/apps/lib/features/messages/ui/screens/message_invite_detail_screen.dart b/apps/lib/features/messages/ui/screens/message_invite_detail_screen.dart index 15fde8c..ff603a5 100644 --- a/apps/lib/features/messages/ui/screens/message_invite_detail_screen.dart +++ b/apps/lib/features/messages/ui/screens/message_invite_detail_screen.dart @@ -1,10 +1,20 @@ import 'package:flutter/material.dart' hide BackButton; import 'package:go_router/go_router.dart'; + +import '../../../../core/di/injection.dart'; import '../../../../core/theme/design_tokens.dart'; +import '../../../../shared/widgets/app_loading_indicator.dart'; import '../../../../shared/widgets/page_header.dart'; +import '../../../../shared/widgets/toast/toast.dart'; +import '../../../../shared/widgets/toast/toast_type.dart'; +import '../../../calendar/data/calendar_api.dart'; +import '../../../users/data/users_api.dart'; +import '../../data/inbox_api.dart'; class MessageInviteDetailScreen extends StatefulWidget { - const MessageInviteDetailScreen({super.key}); + final String inviteId; + + const MessageInviteDetailScreen({super.key, required this.inviteId}); @override State createState() => @@ -12,16 +22,155 @@ class MessageInviteDetailScreen extends StatefulWidget { } class _MessageInviteDetailScreenState extends State { - final _reasonController = TextEditingController(); + late final InboxApi _inboxApi; + late final CalendarApi _calendarApi; + late final UsersApi _usersApi; + + InboxMessageResponse? _message; + String? _calendarTitle; + String? _senderName; + bool _loading = true; + bool _submitting = false; + String? _error; + + bool get _isPending => _message?.status == InboxMessageStatus.pending; @override - void dispose() { - _reasonController.dispose(); - super.dispose(); + void initState() { + super.initState(); + _inboxApi = sl(); + _calendarApi = sl(); + _usersApi = sl(); + _loadDetail(); + } + + Future _loadDetail() async { + setState(() { + _loading = true; + _error = null; + }); + + try { + final results = await Future.wait([ + _inboxApi.getMessages(isRead: false), + _inboxApi.getMessages(isRead: true), + ]); + final messages = [...results[0], ...results[1]]; + InboxMessageResponse? message; + for (final item in messages) { + if (item.id == widget.inviteId) { + message = item; + break; + } + } + if (message == null) { + throw StateError('邀请不存在或已失效'); + } + + String? calendarTitle; + if (message.scheduleItemId != null) { + try { + final event = await _calendarApi.getById(message.scheduleItemId!); + calendarTitle = event.title; + } catch (_) { + calendarTitle = null; + } + } + + String? senderName; + if (message.senderId != null) { + try { + final sender = await _usersApi.getById(message.senderId!); + senderName = sender.username; + } catch (_) { + senderName = null; + } + } + + if (!mounted) { + return; + } + setState(() { + _message = message; + _calendarTitle = calendarTitle; + _senderName = senderName; + _loading = false; + }); + } catch (e) { + if (!mounted) { + return; + } + setState(() { + _error = e.toString().replaceFirst('Bad state: ', ''); + _loading = false; + }); + } + } + + Future _acceptInvite() async { + final message = _message; + final itemId = message?.scheduleItemId; + if (message == null || itemId == null || _submitting) { + return; + } + + setState(() => _submitting = true); + try { + await _calendarApi.acceptSubscription(itemId); + await _inboxApi.markAsRead(message.id); + if (!mounted) { + return; + } + Toast.show(context, '已接受邀请', type: ToastType.success); + await _loadDetail(); + } catch (_) { + if (!mounted) { + return; + } + Toast.show(context, '操作失败,请稍后重试', type: ToastType.error); + } finally { + if (mounted) { + setState(() => _submitting = false); + } + } + } + + Future _rejectInvite() async { + final message = _message; + final itemId = message?.scheduleItemId; + if (message == null || itemId == null || _submitting) { + return; + } + + setState(() => _submitting = true); + try { + await _calendarApi.rejectSubscription(itemId); + await _inboxApi.markAsRead(message.id); + if (!mounted) { + return; + } + Toast.show(context, '已拒绝邀请', type: ToastType.success); + await _loadDetail(); + } catch (_) { + if (!mounted) { + return; + } + Toast.show(context, '操作失败,请稍后重试', type: ToastType.error); + } finally { + if (mounted) { + setState(() => _submitting = false); + } + } } @override Widget build(BuildContext context) { + if (_loading) { + return const Scaffold( + body: SafeArea(child: Center(child: AppLoadingIndicator(size: 22))), + ); + } + return Scaffold( backgroundColor: AppColors.messageBg, body: SafeArea( @@ -40,8 +189,16 @@ class _MessageInviteDetailScreenState extends State { _buildCalendarTip(), const SizedBox(height: 14), _buildActionRow(), - const SizedBox(height: 14), - _buildRejectReasonCard(), + if (_error != null) ...[ + const SizedBox(height: 14), + Text( + _error!, + style: const TextStyle( + fontSize: 12, + color: AppColors.feedbackErrorText, + ), + ), + ], ], ), ), @@ -53,6 +210,21 @@ class _MessageInviteDetailScreenState extends State { } Widget _buildSummaryCard() { + final message = _message; + final statusText = message == null + ? '未知' + : switch (message.status) { + InboxMessageStatus.pending => '待处理', + InboxMessageStatus.accepted => '已接受', + InboxMessageStatus.rejected => '已拒绝', + InboxMessageStatus.dismissed => '已处理', + }; + + final createdAt = message?.createdAt; + final createdAtText = createdAt == null + ? '未知' + : '${createdAt.year}-${createdAt.month.toString().padLeft(2, '0')}-${createdAt.day.toString().padLeft(2, '0')} ${createdAt.hour.toString().padLeft(2, '0')}:${createdAt.minute.toString().padLeft(2, '0')}'; + return Container( width: double.infinity, padding: const EdgeInsets.all(14), @@ -63,8 +235,8 @@ class _MessageInviteDetailScreenState extends State { ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: const [ - Text( + children: [ + const Text( '日历邀请详情', style: TextStyle( fontSize: 16, @@ -72,46 +244,46 @@ class _MessageInviteDetailScreenState extends State { color: AppColors.slate900, ), ), - SizedBox(height: 10), + const SizedBox(height: 10), Text( - '事件:产品评审会', - style: TextStyle( + '事件:${_calendarTitle ?? '未命名日程'}', + style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.slate700, ), ), - SizedBox(height: 10), + const SizedBox(height: 10), Text( - '邀请人:李文浩(产品经理)', - style: TextStyle( + '邀请人:${_senderName ?? '未知用户'}', + style: const TextStyle( fontSize: 13, fontWeight: FontWeight.normal, color: AppColors.slate500, ), ), - SizedBox(height: 10), + const SizedBox(height: 10), Text( - '时间:2026-02-12 14:00 - 15:30', - style: TextStyle( + '消息时间:$createdAtText', + style: const TextStyle( fontSize: 13, fontWeight: FontWeight.normal, color: AppColors.slate500, ), ), - SizedBox(height: 10), + const SizedBox(height: 10), Text( - '地点:3F-A 会议室 / 腾讯会议 231-889-100', - style: TextStyle( + '状态:$statusText', + style: const TextStyle( fontSize: 13, fontWeight: FontWeight.normal, color: AppColors.slate500, ), ), - SizedBox(height: 10), + const SizedBox(height: 10), Text( - '备注:请提前准备本周版本风险与排期结论。', - style: TextStyle( + '邀请ID:${widget.inviteId}', + style: const TextStyle( fontSize: 13, fontWeight: FontWeight.normal, color: AppColors.slate500, @@ -131,13 +303,13 @@ class _MessageInviteDetailScreenState extends State { borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.messageTipBorder), ), - child: Row( - children: const [ + child: const Row( + children: [ Icon(Icons.info_outline, size: 14, color: AppColors.slate500), SizedBox(width: 8), Expanded( child: Text( - '更多附件与相关链接,请在订阅后在日历中查看', + '同意后将加入该日历事件,拒绝后该邀请会被标记为已处理', style: TextStyle( fontSize: 12, fontWeight: FontWeight.normal, @@ -151,22 +323,43 @@ class _MessageInviteDetailScreenState extends State { } Widget _buildActionRow() { + if (!_isPending) { + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12), + decoration: BoxDecoration( + color: AppColors.messageCardBg, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.messageCardBorder), + ), + child: const Text( + '该邀请已处理,无需重复操作', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: AppColors.slate600, + ), + textAlign: TextAlign.center, + ), + ); + } + return SizedBox( height: 46, child: Row( children: [ Expanded( child: GestureDetector( - onTap: _handleReject, + onTap: _submitting ? null : _rejectInvite, child: Container( decoration: BoxDecoration( color: AppColors.messageCardBg, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.messageRejectBorder), ), - child: Row( + child: const Row( mainAxisAlignment: MainAxisAlignment.center, - children: const [ + children: [ Text( '×', style: TextStyle( @@ -192,16 +385,16 @@ class _MessageInviteDetailScreenState extends State { const SizedBox(width: 10), Expanded( child: GestureDetector( - onTap: _handleAccept, + onTap: _submitting ? null : _acceptInvite, child: Container( decoration: BoxDecoration( color: AppColors.messageTagBg, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.messageAcceptBorder), ), - child: Row( + child: const Row( mainAxisAlignment: MainAxisAlignment.center, - children: const [ + children: [ Text( '√', style: TextStyle( @@ -228,66 +421,4 @@ class _MessageInviteDetailScreenState extends State { ), ); } - - Widget _buildRejectReasonCard() { - return Container( - width: double.infinity, - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: AppColors.messageCardBg, - borderRadius: BorderRadius.circular(14), - border: Border.all(color: AppColors.messageReasonBorder), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - '拒绝补充信息(可选)', - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: AppColors.slate600, - ), - ), - const SizedBox(height: 8), - Container( - height: 92, - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: AppColors.messageBtnWrap, - borderRadius: BorderRadius.circular(10), - border: Border.all(color: AppColors.messageInputBorder), - ), - child: TextField( - controller: _reasonController, - maxLines: null, - expands: true, - style: const TextStyle(fontSize: 12, color: AppColors.slate700), - decoration: const InputDecoration( - border: InputBorder.none, - hintText: '可填写拒绝原因,例如:该时间段已有客户会议,无法参加。', - hintStyle: TextStyle( - fontSize: 12, - fontWeight: FontWeight.normal, - color: AppColors.messagePlaceholder, - ), - contentPadding: EdgeInsets.zero, - ), - ), - ), - ], - ), - ); - } - - void _handleReject() { - final reason = _reasonController.text; - debugPrint('Reject with reason: $reason'); - context.pop(); - } - - void _handleAccept() { - debugPrint('Accept invitation'); - context.pop(); - } } diff --git a/apps/lib/features/messages/ui/screens/message_invite_list_screen.dart b/apps/lib/features/messages/ui/screens/message_invite_list_screen.dart index 213b6bd..ed5b449 100644 --- a/apps/lib/features/messages/ui/screens/message_invite_list_screen.dart +++ b/apps/lib/features/messages/ui/screens/message_invite_list_screen.dart @@ -2,17 +2,14 @@ import 'package:flutter/material.dart' hide BackButton; import 'package:go_router/go_router.dart'; import '../../../../core/di/injection.dart'; +import '../../../../core/router/app_routes.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_loading_indicator.dart'; import '../../../../shared/widgets/app_pull_refresh_feedback.dart'; import '../../../../shared/widgets/page_header.dart'; import '../../../../shared/widgets/toast/toast.dart'; import '../../../../shared/widgets/toast/toast_type.dart'; -import '../../../calendar/data/calendar_api.dart'; -import '../../../calendar/data/models/schedule_item_model.dart'; import '../../../friends/data/friends_api.dart'; -import '../../../users/data/models/user_response.dart'; -import '../../../users/data/users_api.dart'; import '../../data/inbox_api.dart'; import '../../ui/widgets/message_action_sheet.dart'; @@ -34,8 +31,6 @@ class MessageInviteListScreen extends StatefulWidget { class _MessageInviteListScreenState extends State { late final InboxApi _inboxApi; late final FriendsApi _friendsApi; - late final CalendarApi _calendarApi; - late final UsersApi _usersApi; List _unreadMessages = []; List _readMessages = []; @@ -48,8 +43,6 @@ class _MessageInviteListScreenState extends State { super.initState(); _inboxApi = sl(); _friendsApi = sl(); - _calendarApi = sl(); - _usersApi = sl(); _loadMessages(); } @@ -144,14 +137,12 @@ class _MessageInviteListScreenState extends State { final type = content['type'] as String?; if (type == 'invite') { - if (message.status.value == 'pending') { - await _showCalendarInviteSheet(message); - } else { - await _showCalendarInviteReadOnlySheet(message); - } + context.push(AppRoutes.messageInviteDetail(message.id)); } else if (type == 'update') { if (message.scheduleItemId != null) { - context.push('/calendar/events/${message.scheduleItemId}'); + context.push( + AppRoutes.calendarEventDetail(message.scheduleItemId!), + ); } } return; @@ -172,103 +163,6 @@ class _MessageInviteListScreenState extends State { return content; } - Future<(String calendarTitle, String senderName)?> _getCalendarInviteInfo( - InboxMessageResponse message, - ) async { - if (message.scheduleItemId == null || message.senderId == null) { - return null; - } - try { - final result = await Future.wait([ - _calendarApi.getById(message.scheduleItemId!), - _usersApi.getById(message.senderId!), - ]); - final calendar = result[0] as ScheduleItemModel; - final sender = result[1] as UserResponse; - return (calendar.title, sender.username); - } catch (e) { - return null; - } - } - - Future _showCalendarInviteSheet(InboxMessageResponse message) async { - final itemId = message.scheduleItemId; - if (itemId == null) return; - - final info = await _getCalendarInviteInfo(message); - final title = info != null ? '${info.$2} 邀请你加入日历' : '日历邀请'; - final description = info?.$1; - - if (!mounted) return; - - showModalBottomSheet( - context: context, - backgroundColor: Colors.transparent, - builder: (ctx) => MessageActionSheet( - title: title, - description: description, - icon: Icons.calendar_today, - iconColor: AppColors.blue500, - onAccept: () async { - try { - await _calendarApi.acceptSubscription(itemId); - await _inboxApi.markAsRead(message.id); - if (mounted) { - Toast.show(context, '已接受', type: ToastType.success); - _loadMessages(); - } - } catch (e) { - if (mounted) { - Toast.show(context, '操作失败', type: ToastType.error); - } - } - }, - onDecline: () async { - try { - await _calendarApi.rejectSubscription(itemId); - await _inboxApi.markAsRead(message.id); - if (mounted) { - Toast.show(context, '已拒绝', type: ToastType.success); - _loadMessages(); - } - } catch (e) { - if (mounted) { - Toast.show(context, '操作失败', type: ToastType.error); - } - } - }, - ), - ); - } - - Future _showCalendarInviteReadOnlySheet( - InboxMessageResponse message, - ) async { - final itemId = message.scheduleItemId; - if (itemId == null) return; - - final info = await _getCalendarInviteInfo(message); - final title = info != null ? '${info.$2} 邀请你加入日历' : '日历邀请'; - final description = info?.$1; - - final statusText = message.status.value == 'accepted' ? '已接受' : '已拒绝'; - - if (!mounted) return; - - showModalBottomSheet( - context: context, - backgroundColor: Colors.transparent, - builder: (ctx) => MessageActionSheet( - title: title, - description: description, - statusText: statusText, - isReadOnly: true, - icon: Icons.calendar_today, - iconColor: AppColors.blue500, - ), - ); - } - void _showFriendRequestSheet( MessageWithFriend item, { bool isReadOnly = false,