feat(apps): 重构 UI 架构为 presentation 层并新增 l10n 国际化支持

This commit is contained in:
qzl
2026-03-27 14:05:03 +08:00
parent b1f0eb8921
commit c592cc7854
178 changed files with 10748 additions and 5764 deletions
@@ -1,4 +1,4 @@
import 'package:social_app/core/api/i_api_client.dart';
import 'package:social_app/core/network/i_api_client.dart';
class InboxApi {
final IApiClient _client;
@@ -1,14 +1,16 @@
import 'package:flutter/material.dart' hide BackButton;
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import '../../../../core/di/injection.dart';
import '../../../../app/di/injection.dart';
import '../../../../core/l10n/l10n.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 '../../../contacts/data/users/users_api.dart';
import '../../data/inbox_api.dart';
class MessageInviteDetailScreen extends StatefulWidget {
@@ -64,7 +66,7 @@ class _MessageInviteDetailScreenState extends State<MessageInviteDetailScreen> {
}
}
if (message == null) {
throw StateError('邀请不存在或已失效');
throw StateError(L10n.current.messagesInviteDetailNotFound);
}
String? calendarTitle;
@@ -121,13 +123,21 @@ class _MessageInviteDetailScreenState extends State<MessageInviteDetailScreen> {
if (!mounted) {
return;
}
Toast.show(context, '已接受邀请', type: ToastType.success);
Toast.show(
context,
context.l10n.messagesInviteAcceptedToast,
type: ToastType.success,
);
await _loadDetail();
} catch (_) {
if (!mounted) {
return;
}
Toast.show(context, '操作失败,请稍后重试', type: ToastType.error);
Toast.show(
context,
context.l10n.messagesInviteOperationFailed,
type: ToastType.error,
);
} finally {
if (mounted) {
setState(() => _submitting = false);
@@ -149,13 +159,21 @@ class _MessageInviteDetailScreenState extends State<MessageInviteDetailScreen> {
if (!mounted) {
return;
}
Toast.show(context, '已拒绝邀请', type: ToastType.success);
Toast.show(
context,
context.l10n.messagesInviteRejectedToast,
type: ToastType.success,
);
await _loadDetail();
} catch (_) {
if (!mounted) {
return;
}
Toast.show(context, '操作失败,请稍后重试', type: ToastType.error);
Toast.show(
context,
context.l10n.messagesInviteOperationFailed,
type: ToastType.error,
);
} finally {
if (mounted) {
setState(() => _submitting = false);
@@ -212,18 +230,21 @@ class _MessageInviteDetailScreenState extends State<MessageInviteDetailScreen> {
Widget _buildSummaryCard() {
final message = _message;
final statusText = message == null
? '未知'
? context.l10n.commonUnknown
: switch (message.status) {
InboxMessageStatus.pending => '待处理',
InboxMessageStatus.accepted => '已接受',
InboxMessageStatus.rejected => '已拒绝',
InboxMessageStatus.dismissed => '已处理',
InboxMessageStatus.pending => context.l10n.messagesStatusPending,
InboxMessageStatus.accepted =>
context.l10n.messagesInviteStatusAccepted,
InboxMessageStatus.rejected =>
context.l10n.messagesInviteStatusRejected,
InboxMessageStatus.dismissed =>
context.l10n.messagesInviteStatusHandled,
};
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')}';
? context.l10n.commonUnknown
: DateFormat.yMd(context.l10n.localeName).add_Hm().format(createdAt);
return Container(
width: double.infinity,
@@ -236,8 +257,8 @@ class _MessageInviteDetailScreenState extends State<MessageInviteDetailScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'日历邀请详情',
Text(
context.l10n.messagesInviteDetailTitle,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
@@ -246,7 +267,9 @@ class _MessageInviteDetailScreenState extends State<MessageInviteDetailScreen> {
),
const SizedBox(height: 10),
Text(
'事件:${_calendarTitle ?? '未命名日程'}',
context.l10n.messagesInviteEvent(
_calendarTitle ?? context.l10n.messagesInviteUnnamedEvent,
),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
@@ -255,7 +278,9 @@ class _MessageInviteDetailScreenState extends State<MessageInviteDetailScreen> {
),
const SizedBox(height: 10),
Text(
'邀请人:${_senderName ?? '未知用户'}',
context.l10n.messagesInviteSender(
_senderName ?? context.l10n.messagesInviteUnknownUser,
),
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.normal,
@@ -264,7 +289,7 @@ class _MessageInviteDetailScreenState extends State<MessageInviteDetailScreen> {
),
const SizedBox(height: 10),
Text(
'消息时间:$createdAtText',
context.l10n.messagesInviteTime(createdAtText),
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.normal,
@@ -273,7 +298,7 @@ class _MessageInviteDetailScreenState extends State<MessageInviteDetailScreen> {
),
const SizedBox(height: 10),
Text(
'状态:$statusText',
context.l10n.messagesInviteStatus(statusText),
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.normal,
@@ -282,7 +307,7 @@ class _MessageInviteDetailScreenState extends State<MessageInviteDetailScreen> {
),
const SizedBox(height: 10),
Text(
'邀请ID${widget.inviteId}',
context.l10n.messagesInviteId(widget.inviteId),
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.normal,
@@ -303,14 +328,14 @@ class _MessageInviteDetailScreenState extends State<MessageInviteDetailScreen> {
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.messageTipBorder),
),
child: const Row(
child: Row(
children: [
Icon(Icons.info_outline, size: 14, color: AppColors.slate500),
SizedBox(width: 8),
const Icon(Icons.info_outline, size: 14, color: AppColors.slate500),
const SizedBox(width: 8),
Expanded(
child: Text(
'同意后将加入该日历事件,拒绝后该邀请会被标记为已处理',
style: TextStyle(
context.l10n.messagesInviteTip,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.normal,
color: AppColors.slate500,
@@ -332,8 +357,8 @@ class _MessageInviteDetailScreenState extends State<MessageInviteDetailScreen> {
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.messageCardBorder),
),
child: const Text(
'该邀请已处理,无需重复操作',
child: Text(
context.l10n.messagesInviteAlreadyHandled,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
@@ -357,10 +382,10 @@ class _MessageInviteDetailScreenState extends State<MessageInviteDetailScreen> {
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.messageRejectBorder),
),
child: const Row(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
const Text(
'×',
style: TextStyle(
fontSize: 15,
@@ -368,10 +393,10 @@ class _MessageInviteDetailScreenState extends State<MessageInviteDetailScreen> {
color: AppColors.red400,
),
),
SizedBox(width: 6),
const SizedBox(width: 6),
Text(
'拒绝',
style: TextStyle(
context.l10n.messagesReject,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.red400,
@@ -392,10 +417,10 @@ class _MessageInviteDetailScreenState extends State<MessageInviteDetailScreen> {
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.messageAcceptBorder),
),
child: const Row(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
const Text(
'',
style: TextStyle(
fontSize: 15,
@@ -403,10 +428,10 @@ class _MessageInviteDetailScreenState extends State<MessageInviteDetailScreen> {
color: AppColors.blue600,
),
),
SizedBox(width: 6),
const SizedBox(width: 6),
Text(
'同意',
style: TextStyle(
context.l10n.messagesAccept,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.blue600,
@@ -1,17 +1,18 @@
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 '../../../../app/di/injection.dart';
import '../../../../app/router/app_routes.dart';
import '../../../../core/l10n/l10n.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 '../../../friends/data/friends_api.dart';
import '../../../contacts/data/friends_api.dart';
import '../../data/inbox_api.dart';
import '../../ui/widgets/message_action_sheet.dart';
import '../../presentation/widgets/message_action_sheet.dart';
class MessageWithFriend {
final InboxMessageResponse message;
@@ -108,7 +109,11 @@ class _MessageInviteListScreenState extends State<MessageInviteListScreen> {
_isLoading = false;
_isPullRefreshing = false;
});
Toast.show(context, '消息加载失败,请稍后重试', type: ToastType.error);
Toast.show(
context,
context.l10n.messagesLoadFailed,
type: ToastType.error,
);
}
}
@@ -148,7 +153,11 @@ class _MessageInviteListScreenState extends State<MessageInviteListScreen> {
return;
case InboxMessageType.friendRequest:
if (item.friendRequest == null) {
Toast.show(context, '发送者信息加载失败,请下拉重试', type: ToastType.error);
Toast.show(
context,
context.l10n.messagesSenderLoadFailed,
type: ToastType.error,
);
return;
}
_showFriendRequestSheet(item, isReadOnly: message.isRead);
@@ -171,14 +180,16 @@ class _MessageInviteListScreenState extends State<MessageInviteListScreen> {
final friendRequest = item.friendRequest;
if (friendRequest == null) return;
final title = '${friendRequest.sender.username} 请求添加您为好友';
final title = context.l10n.messagesFriendRequestTitle(
friendRequest.sender.username,
);
final description = message.content?['message'] as String?;
final statusText = isReadOnly
? (friendRequest.status == 'accepted'
? '已接受'
? context.l10n.messagesInviteStatusAccepted
: friendRequest.status == 'rejected'
? '已拒绝'
: '已处理')
? context.l10n.messagesInviteStatusRejected
: context.l10n.messagesInviteStatusHandled)
: null;
showModalBottomSheet<void>(
@@ -213,7 +224,11 @@ class _MessageInviteListScreenState extends State<MessageInviteListScreen> {
final message = item.message;
final friendshipId = message.friendshipId;
if (friendshipId == null) {
Toast.show(context, '好友请求数据缺失', type: ToastType.error);
Toast.show(
context,
context.l10n.messagesFriendRequestMissing,
type: ToastType.error,
);
return;
}
@@ -221,19 +236,31 @@ class _MessageInviteListScreenState extends State<MessageInviteListScreen> {
if (accept) {
await _friendsApi.acceptRequest(friendshipId);
if (mounted) {
Toast.show(context, '已接受好友请求', type: ToastType.success);
Toast.show(
context,
context.l10n.messagesAcceptedFriendRequest,
type: ToastType.success,
);
}
} else {
await _friendsApi.declineRequest(friendshipId);
if (mounted) {
Toast.show(context, '已拒绝好友请求', type: ToastType.success);
Toast.show(
context,
context.l10n.messagesRejectedFriendRequest,
type: ToastType.success,
);
}
}
await _inboxApi.markAsRead(message.id);
await _loadMessages();
} catch (e) {
if (mounted) {
Toast.show(context, '处理失败,请稍后重试', type: ToastType.error);
Toast.show(
context,
context.l10n.messagesActionFailed,
type: ToastType.error,
);
}
}
}
@@ -289,9 +316,21 @@ class _MessageInviteListScreenState extends State<MessageInviteListScreen> {
),
child: Row(
children: [
Expanded(child: _buildTab(0, '未读', Icons.mark_email_unread_outlined)),
Expanded(
child: _buildTab(
0,
context.l10n.messagesTabUnread,
Icons.mark_email_unread_outlined,
),
),
const SizedBox(width: 4),
Expanded(child: _buildTab(1, '已读', Icons.mark_email_read_outlined)),
Expanded(
child: _buildTab(
1,
context.l10n.messagesTabRead,
Icons.mark_email_read_outlined,
),
),
],
),
);
@@ -412,7 +451,9 @@ class _MessageInviteListScreenState extends State<MessageInviteListScreen> {
),
const SizedBox(height: 16),
Text(
isUnread ? '暂无未读消息' : '暂无已读消息',
isUnread
? context.l10n.messagesEmptyUnreadTitle
: context.l10n.messagesEmptyReadTitle,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
@@ -421,7 +462,9 @@ class _MessageInviteListScreenState extends State<MessageInviteListScreen> {
),
const SizedBox(height: 8),
Text(
isUnread ? '有新消息时会在这里显示' : '处理过的消息会显示在这里',
isUnread
? context.l10n.messagesEmptyUnreadDesc
: context.l10n.messagesEmptyReadDesc,
style: const TextStyle(fontSize: 13, color: AppColors.slate400),
),
],
@@ -506,15 +549,17 @@ class _MessageCard extends StatelessWidget {
String _title() {
if (message.messageType == InboxMessageType.friendRequest) {
if (friendRequest == null) {
return '好友请求信息加载失败';
return L10n.current.messagesFriendRequestLoadFailed;
}
return '${friendRequest!.sender.username} 请求添加您为好友';
return L10n.current.messagesFriendRequestTitle(
friendRequest!.sender.username,
);
}
if (message.messageType == InboxMessageType.calendar) {
final data = message.content;
return data?['title'] as String? ?? '日历邀请';
return data?['title'] as String? ?? L10n.current.messagesCalendarInvite;
}
return '系统消息';
return L10n.current.messagesSystemMessage;
}
String _content() {
@@ -523,23 +568,24 @@ class _MessageCard extends StatelessWidget {
if (message.content != null) {
data = message.content;
}
if (data == null) return '点击查看详情';
if (data == null) return L10n.current.messagesTapToView;
final type = data['type'] as String?;
if (type == 'invite') {
final status = message.status.value;
if (status == 'pending') {
return '邀请您加入日历';
return L10n.current.messagesInviteJoinCalendar;
} else if (status == 'accepted') {
return '已接受日历邀请';
return L10n.current.messagesInviteAccepted;
} else if (status == 'rejected') {
return '已拒绝日历邀请';
return L10n.current.messagesInviteRejected;
}
} else if (type == 'update') {
return '更新了日历事件';
return L10n.current.messagesCalendarUpdated;
}
return '点击查看详情';
return L10n.current.messagesTapToView;
}
return message.content?['message'] as String? ?? '点击查看详情';
return message.content?['message'] as String? ??
L10n.current.messagesTapToView;
}
}
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:social_app/core/l10n/l10n.dart';
import '../../../../core/theme/design_tokens.dart';
import '../../../../shared/widgets/app_button.dart';
@@ -23,6 +24,8 @@ class CalendarInviteCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Container(
margin: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
@@ -52,17 +55,22 @@ class CalendarInviteCard extends StatelessWidget {
),
),
const SizedBox(width: AppSpacing.sm),
const Expanded(
Expanded(
child: Text(
'日历邀请',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14),
l10n.messagesCalendarCardInviteTitle,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
],
),
const SizedBox(height: AppSpacing.sm),
Text(
eventTitle != null ? '邀请你访问 "$eventTitle"' : '邀请你访问日历',
eventTitle != null
? l10n.messagesCalendarCardInviteWithTitle(eventTitle!)
: l10n.messagesCalendarCardInviteWithoutTitle,
style: const TextStyle(fontSize: 14, color: AppColors.slate700),
),
const SizedBox(height: AppSpacing.md),
@@ -70,14 +78,17 @@ class CalendarInviteCard extends StatelessWidget {
children: [
Expanded(
child: AppButton(
text: '拒绝',
text: l10n.messagesReject,
isOutlined: true,
onPressed: onReject,
),
),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: AppButton(text: '接受', onPressed: onAccept),
child: AppButton(
text: l10n.messagesAccept,
onPressed: onAccept,
),
),
],
),
@@ -100,6 +111,8 @@ class CalendarUpdateCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return GestureDetector(
onTap: onTap,
child: Container(
@@ -133,7 +146,9 @@ class CalendarUpdateCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
eventTitle != null ? '$eventTitle 已更新' : '日历事件已更新',
eventTitle != null
? l10n.messagesCalendarCardUpdatedWithTitle(eventTitle!)
: l10n.messagesCalendarCardUpdatedWithoutTitle,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
@@ -141,7 +156,7 @@ class CalendarUpdateCard extends StatelessWidget {
),
const SizedBox(height: 2),
Text(
_formatTime(message.createdAt),
_formatTime(context, message.createdAt),
style: const TextStyle(
fontSize: 12,
color: AppColors.slate500,
@@ -157,17 +172,18 @@ class CalendarUpdateCard extends StatelessWidget {
);
}
String _formatTime(DateTime time) {
String _formatTime(BuildContext context, DateTime time) {
final l10n = context.l10n;
final now = DateTime.now();
final diff = now.difference(time);
if (diff.inMinutes < 60) {
return '${diff.inMinutes}分钟前';
return l10n.messagesCalendarCardTimeMinutesAgo(diff.inMinutes);
} else if (diff.inHours < 24) {
return '${diff.inHours}小时前';
return l10n.messagesCalendarCardTimeHoursAgo(diff.inHours);
} else if (diff.inDays < 7) {
return '${diff.inDays}天前';
return l10n.messagesCalendarCardTimeDaysAgo(diff.inDays);
} else {
return '${time.month}${time.day}';
return l10n.messagesCalendarCardTimeDate(time.month, time.day);
}
}
}
@@ -184,6 +200,8 @@ class CalendarDeleteCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Container(
margin: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
@@ -212,7 +230,9 @@ class CalendarDeleteCard extends StatelessWidget {
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Text(
eventTitle != null ? '$eventTitle 已删除' : '日历事件已删除',
eventTitle != null
? l10n.messagesCalendarCardDeletedWithTitle(eventTitle!)
: l10n.messagesCalendarCardDeletedWithoutTitle,
style: const TextStyle(fontSize: 14, color: AppColors.slate500),
),
),
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../../../../core/theme/design_tokens.dart';
import '../../../../core/l10n/l10n.dart';
import '../../../../shared/widgets/app_button.dart';
class MessageActionSheet extends StatelessWidget {
@@ -98,7 +99,7 @@ class MessageActionSheet extends StatelessWidget {
children: [
Expanded(
child: AppButton(
text: '拒绝',
text: context.l10n.messagesReject,
isOutlined: true,
onPressed: () {
Navigator.pop(context);
@@ -109,7 +110,7 @@ class MessageActionSheet extends StatelessWidget {
const SizedBox(width: AppSpacing.md),
Expanded(
child: AppButton(
text: '接受',
text: context.l10n.messagesAccept,
onPressed: () {
Navigator.pop(context);
onAccept?.call();