feat(apps): 更新消息邀请详情和认证流程路由

This commit is contained in:
zl-q
2026-03-19 00:51:58 +08:00
parent 81cbc14219
commit 29a4ea5294
4 changed files with 235 additions and 208 deletions
@@ -4,6 +4,7 @@ import 'package:formz/formz.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/theme/design_tokens.dart'; import '../../../../core/theme/design_tokens.dart';
import '../../../../shared/widgets/app_button.dart'; import '../../../../shared/widgets/app_button.dart';
import '../../../../shared/widgets/banner/app_banner.dart'; import '../../../../shared/widgets/banner/app_banner.dart';
@@ -59,7 +60,7 @@ class _LoginViewState extends State<LoginView> {
final response = await cubit.submit(); final response = await cubit.submit();
if (response != null && mounted) { if (response != null && mounted) {
context.read<AuthBloc>().add(AuthLoggedIn(user: response.user)); context.read<AuthBloc>().add(AuthLoggedIn(user: response.user));
context.go('/home'); context.go(AppRoutes.homeMain);
} }
} }
@@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:formz/formz.dart'; import 'package:formz/formz.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import '../../../../core/router/app_routes.dart';
import '../../../../core/theme/design_tokens.dart'; import '../../../../core/theme/design_tokens.dart';
import '../../../../shared/widgets/app_button.dart'; import '../../../../shared/widgets/app_button.dart';
import '../../../../shared/widgets/banner/app_banner.dart'; import '../../../../shared/widgets/banner/app_banner.dart';
@@ -93,7 +94,7 @@ class _RegisterVerificationViewState extends State<RegisterVerificationView> {
final response = await cubit.submitStep2(); final response = await cubit.submitStep2();
if (response != null && mounted) { if (response != null && mounted) {
context.read<AuthBloc>().add(AuthLoggedIn(user: response.user)); context.read<AuthBloc>().add(AuthLoggedIn(user: response.user));
context.go('/home'); context.go(AppRoutes.homeMain);
} }
} }
@@ -1,10 +1,20 @@
import 'package:flutter/material.dart' hide BackButton; import 'package:flutter/material.dart' hide BackButton;
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import '../../../../core/di/injection.dart';
import '../../../../core/theme/design_tokens.dart'; import '../../../../core/theme/design_tokens.dart';
import '../../../../shared/widgets/app_loading_indicator.dart';
import '../../../../shared/widgets/page_header.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 { class MessageInviteDetailScreen extends StatefulWidget {
const MessageInviteDetailScreen({super.key}); final String inviteId;
const MessageInviteDetailScreen({super.key, required this.inviteId});
@override @override
State<MessageInviteDetailScreen> createState() => State<MessageInviteDetailScreen> createState() =>
@@ -12,16 +22,155 @@ class MessageInviteDetailScreen extends StatefulWidget {
} }
class _MessageInviteDetailScreenState extends State<MessageInviteDetailScreen> { class _MessageInviteDetailScreenState extends State<MessageInviteDetailScreen> {
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 @override
void dispose() { void initState() {
_reasonController.dispose(); super.initState();
super.dispose(); _inboxApi = sl<InboxApi>();
_calendarApi = sl<CalendarApi>();
_usersApi = sl<UsersApi>();
_loadDetail();
}
Future<void> _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<void> _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<void> _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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_loading) {
return const Scaffold(
body: SafeArea(child: Center(child: AppLoadingIndicator(size: 22))),
);
}
return Scaffold( return Scaffold(
backgroundColor: AppColors.messageBg, backgroundColor: AppColors.messageBg,
body: SafeArea( body: SafeArea(
@@ -40,8 +189,16 @@ class _MessageInviteDetailScreenState extends State<MessageInviteDetailScreen> {
_buildCalendarTip(), _buildCalendarTip(),
const SizedBox(height: 14), const SizedBox(height: 14),
_buildActionRow(), _buildActionRow(),
const SizedBox(height: 14), if (_error != null) ...[
_buildRejectReasonCard(), const SizedBox(height: 14),
Text(
_error!,
style: const TextStyle(
fontSize: 12,
color: AppColors.feedbackErrorText,
),
),
],
], ],
), ),
), ),
@@ -53,6 +210,21 @@ class _MessageInviteDetailScreenState extends State<MessageInviteDetailScreen> {
} }
Widget _buildSummaryCard() { 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( return Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(14), padding: const EdgeInsets.all(14),
@@ -63,8 +235,8 @@ class _MessageInviteDetailScreenState extends State<MessageInviteDetailScreen> {
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: const [ children: [
Text( const Text(
'日历邀请详情', '日历邀请详情',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
@@ -72,46 +244,46 @@ class _MessageInviteDetailScreenState extends State<MessageInviteDetailScreen> {
color: AppColors.slate900, color: AppColors.slate900,
), ),
), ),
SizedBox(height: 10), const SizedBox(height: 10),
Text( Text(
'事件:产品评审会', '事件:${_calendarTitle ?? '未命名日程'}',
style: TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: AppColors.slate700, color: AppColors.slate700,
), ),
), ),
SizedBox(height: 10), const SizedBox(height: 10),
Text( Text(
'邀请人:李文浩(产品经理)', '邀请人:${_senderName ?? '未知用户'}',
style: TextStyle( style: const TextStyle(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
color: AppColors.slate500, color: AppColors.slate500,
), ),
), ),
SizedBox(height: 10), const SizedBox(height: 10),
Text( Text(
'时间:2026-02-12 14:00 - 15:30', '消息时间:$createdAtText',
style: TextStyle( style: const TextStyle(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
color: AppColors.slate500, color: AppColors.slate500,
), ),
), ),
SizedBox(height: 10), const SizedBox(height: 10),
Text( Text(
'地点:3F-A 会议室 / 腾讯会议 231-889-100', '状态:$statusText',
style: TextStyle( style: const TextStyle(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
color: AppColors.slate500, color: AppColors.slate500,
), ),
), ),
SizedBox(height: 10), const SizedBox(height: 10),
Text( Text(
'备注:请提前准备本周版本风险与排期结论。', '邀请ID${widget.inviteId}',
style: TextStyle( style: const TextStyle(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
color: AppColors.slate500, color: AppColors.slate500,
@@ -131,13 +303,13 @@ class _MessageInviteDetailScreenState extends State<MessageInviteDetailScreen> {
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.messageTipBorder), border: Border.all(color: AppColors.messageTipBorder),
), ),
child: Row( child: const Row(
children: const [ children: [
Icon(Icons.info_outline, size: 14, color: AppColors.slate500), Icon(Icons.info_outline, size: 14, color: AppColors.slate500),
SizedBox(width: 8), SizedBox(width: 8),
Expanded( Expanded(
child: Text( child: Text(
'更多附件与相关链接,请在订阅后在日历中查看', '同意后将加入该日历事件,拒绝后该邀请会被标记为已处理',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
@@ -151,22 +323,43 @@ class _MessageInviteDetailScreenState extends State<MessageInviteDetailScreen> {
} }
Widget _buildActionRow() { 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( return SizedBox(
height: 46, height: 46,
child: Row( child: Row(
children: [ children: [
Expanded( Expanded(
child: GestureDetector( child: GestureDetector(
onTap: _handleReject, onTap: _submitting ? null : _rejectInvite,
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.messageCardBg, color: AppColors.messageCardBg,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.messageRejectBorder), border: Border.all(color: AppColors.messageRejectBorder),
), ),
child: Row( child: const Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: const [ children: [
Text( Text(
'×', '×',
style: TextStyle( style: TextStyle(
@@ -192,16 +385,16 @@ class _MessageInviteDetailScreenState extends State<MessageInviteDetailScreen> {
const SizedBox(width: 10), const SizedBox(width: 10),
Expanded( Expanded(
child: GestureDetector( child: GestureDetector(
onTap: _handleAccept, onTap: _submitting ? null : _acceptInvite,
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.messageTagBg, color: AppColors.messageTagBg,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.messageAcceptBorder), border: Border.all(color: AppColors.messageAcceptBorder),
), ),
child: Row( child: const Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: const [ children: [
Text( Text(
'', '',
style: TextStyle( style: TextStyle(
@@ -228,66 +421,4 @@ class _MessageInviteDetailScreenState extends State<MessageInviteDetailScreen> {
), ),
); );
} }
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();
}
} }
@@ -2,17 +2,14 @@ import 'package:flutter/material.dart' hide BackButton;
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/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_pull_refresh_feedback.dart'; import '../../../../shared/widgets/app_pull_refresh_feedback.dart';
import '../../../../shared/widgets/page_header.dart'; import '../../../../shared/widgets/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 '../../../calendar/data/calendar_api.dart';
import '../../../calendar/data/models/schedule_item_model.dart';
import '../../../friends/data/friends_api.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 '../../data/inbox_api.dart';
import '../../ui/widgets/message_action_sheet.dart'; import '../../ui/widgets/message_action_sheet.dart';
@@ -34,8 +31,6 @@ class MessageInviteListScreen extends StatefulWidget {
class _MessageInviteListScreenState extends State<MessageInviteListScreen> { class _MessageInviteListScreenState extends State<MessageInviteListScreen> {
late final InboxApi _inboxApi; late final InboxApi _inboxApi;
late final FriendsApi _friendsApi; late final FriendsApi _friendsApi;
late final CalendarApi _calendarApi;
late final UsersApi _usersApi;
List<MessageWithFriend> _unreadMessages = []; List<MessageWithFriend> _unreadMessages = [];
List<MessageWithFriend> _readMessages = []; List<MessageWithFriend> _readMessages = [];
@@ -48,8 +43,6 @@ class _MessageInviteListScreenState extends State<MessageInviteListScreen> {
super.initState(); super.initState();
_inboxApi = sl<InboxApi>(); _inboxApi = sl<InboxApi>();
_friendsApi = sl<FriendsApi>(); _friendsApi = sl<FriendsApi>();
_calendarApi = sl<CalendarApi>();
_usersApi = sl<UsersApi>();
_loadMessages(); _loadMessages();
} }
@@ -144,14 +137,12 @@ class _MessageInviteListScreenState extends State<MessageInviteListScreen> {
final type = content['type'] as String?; final type = content['type'] as String?;
if (type == 'invite') { if (type == 'invite') {
if (message.status.value == 'pending') { context.push(AppRoutes.messageInviteDetail(message.id));
await _showCalendarInviteSheet(message);
} else {
await _showCalendarInviteReadOnlySheet(message);
}
} else if (type == 'update') { } else if (type == 'update') {
if (message.scheduleItemId != null) { if (message.scheduleItemId != null) {
context.push('/calendar/events/${message.scheduleItemId}'); context.push(
AppRoutes.calendarEventDetail(message.scheduleItemId!),
);
} }
} }
return; return;
@@ -172,103 +163,6 @@ class _MessageInviteListScreenState extends State<MessageInviteListScreen> {
return content; 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<void> _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<void>(
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<void> _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<void>(
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( void _showFriendRequestSheet(
MessageWithFriend item, { MessageWithFriend item, {
bool isReadOnly = false, bool isReadOnly = false,