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,5 +1,6 @@
import 'package:flutter/material.dart';
import '../../core/l10n/l10n.dart';
import '../../core/theme/design_tokens.dart';
import 'app_loading_indicator.dart';
@@ -7,16 +8,17 @@ class AppPullRefreshFeedback extends StatelessWidget {
const AppPullRefreshFeedback({
super.key,
required this.visible,
this.label = '正在刷新',
this.label,
this.margin = const EdgeInsets.only(top: AppSpacing.sm),
});
final bool visible;
final String label;
final String? label;
final EdgeInsetsGeometry margin;
@override
Widget build(BuildContext context) {
final resolvedLabel = label ?? context.l10n.commonRefreshing;
return IgnorePointer(
child: AnimatedOpacity(
opacity: visible ? 1 : 0,
@@ -44,7 +46,7 @@ class AppPullRefreshFeedback extends StatelessWidget {
),
const SizedBox(width: AppSpacing.sm),
Text(
label,
resolvedLabel,
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: AppColors.slate600,
fontWeight: FontWeight.w500,
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../../core/l10n/l10n.dart';
import '../../core/theme/design_tokens.dart';
import 'app_button.dart';
@@ -69,7 +70,7 @@ Future<T?> showAppSelectionSheet<T>(
child: SizedBox(
height: 48,
child: AppButton(
text: '取消',
text: context.l10n.commonCancel,
isOutlined: true,
onPressed: () => Navigator.of(sheetContext).pop(),
),
@@ -21,7 +21,7 @@ class AppBanner extends StatelessWidget {
Widget build(BuildContext context) {
if (!visible) return const SizedBox.shrink();
final config = ToastTypeConfig.fromType(type);
final config = ToastTypeConfig.fromType(context, type);
return Container(
width: double.infinity,
+4 -3
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import '../../../core/l10n/l10n.dart';
import '../../../core/theme/design_tokens.dart';
enum MessageSender { user, ai }
@@ -77,11 +78,11 @@ class ChatBubble extends StatelessWidget {
String dateStr;
if (msgDate == today) {
dateStr = '今天';
dateStr = L10n.current.chatTimestampToday;
} else if (msgDate == today.subtract(const Duration(days: 1))) {
dateStr = '昨天';
dateStr = L10n.current.chatTimestampYesterday;
} else {
dateStr = '${time.month}${time.day}';
dateStr = L10n.current.chatTimestampMonthDay(time.month, time.day);
}
final timeStr =
+8 -4
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../../core/l10n/l10n.dart';
import '../../core/theme/design_tokens.dart';
import 'app_button.dart';
@@ -7,10 +8,13 @@ Future<bool> showConfirmSheet(
BuildContext context, {
required String title,
required String message,
String confirmText = '确认',
String cancelText = '取消',
String? confirmText,
String? cancelText,
bool isDestructive = false,
}) async {
final l10n = context.l10n;
final resolvedConfirmText = confirmText ?? l10n.commonConfirm;
final resolvedCancelText = cancelText ?? l10n.commonCancel;
final result = await showModalBottomSheet<bool>(
context: context,
isScrollControlled: true,
@@ -64,7 +68,7 @@ Future<bool> showConfirmSheet(
borderRadius: BorderRadius.circular(AppRadius.full),
),
child: Text(
confirmText,
resolvedConfirmText,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
@@ -78,7 +82,7 @@ Future<bool> showConfirmSheet(
SizedBox(
height: 52,
child: AppButton(
text: cancelText,
text: resolvedCancelText,
isOutlined: true,
onPressed: () => Navigator.of(sheetContext).pop(false),
),
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../../core/l10n/l10n.dart';
import '../../core/theme/design_tokens.dart';
import 'app_button.dart';
@@ -74,7 +75,7 @@ Future<bool> showDestructiveActionSheet(
SizedBox(
height: 52,
child: AppButton(
text: '取消',
text: context.l10n.commonCancel,
isOutlined: true,
onPressed: () => Navigator.of(sheetContext).pop(false),
),
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../../core/l10n/l10n.dart';
import '../../core/theme/design_tokens.dart';
import 'app_button.dart';
@@ -30,7 +31,7 @@ class ErrorRetrySurface extends StatelessWidget {
style: const TextStyle(color: AppColors.red500),
),
const SizedBox(height: AppSpacing.md),
AppButton(text: '重试', onPressed: onRetry),
AppButton(text: context.l10n.commonRetry, onPressed: onRetry),
],
),
),
+22 -13
View File
@@ -2,6 +2,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import '../../core/l10n/l10n.dart';
import '../../core/theme/design_tokens.dart';
import 'app_loading_indicator.dart';
@@ -37,10 +38,10 @@ class MessageComposer extends StatelessWidget {
required this.onHoldToSpeakCancel,
required this.textInputChild,
required this.recordingAnimation,
this.holdToSpeakText = '按住说话',
this.recordingText = '松开发送',
this.transcribingText = '语音识别中...',
this.recordingHintText = '松开发送,上滑取消',
this.holdToSpeakText,
this.recordingText,
this.transcribingText,
this.recordingHintText,
this.showRecordingInlineFeedback = true,
});
@@ -58,10 +59,10 @@ class MessageComposer extends StatelessWidget {
final VoidCallback onHoldToSpeakCancel;
final Widget textInputChild;
final Widget recordingAnimation;
final String holdToSpeakText;
final String recordingText;
final String transcribingText;
final String recordingHintText;
final String? holdToSpeakText;
final String? recordingText;
final String? transcribingText;
final String? recordingHintText;
final bool showRecordingInlineFeedback;
bool get _isHoldMode => mode == MessageComposerMode.holdToSpeak;
@@ -193,12 +194,20 @@ class MessageComposer extends StatelessWidget {
}
Widget _buildHoldToSpeakContent() {
final l10n = L10n.current;
final resolvedRecordingText =
recordingText ?? l10n.homeRecordingReleaseSend;
final resolvedRecordingHintText =
recordingHintText ?? l10n.homeRecordingHintReleaseSend;
final resolvedTranscribingText = transcribingText ?? l10n.homeTranscribing;
final resolvedHoldToSpeakText = holdToSpeakText ?? l10n.homeHoldToSpeakText;
if (_isRecording) {
if (!showRecordingInlineFeedback) {
return Align(
alignment: Alignment.center,
child: Text(
recordingText,
resolvedRecordingText,
style: const TextStyle(color: AppColors.slate700),
),
);
@@ -210,12 +219,12 @@ class MessageComposer extends StatelessWidget {
recordingAnimation,
const SizedBox(height: AppSpacing.xs),
Text(
recordingText,
resolvedRecordingText,
style: const TextStyle(color: AppColors.slate700),
),
const SizedBox(height: AppSpacing.xs),
Text(
recordingHintText,
resolvedRecordingHintText,
key: messageComposerRecordingHintKey,
style: const TextStyle(color: AppColors.slate500),
),
@@ -227,7 +236,7 @@ class MessageComposer extends StatelessWidget {
return Align(
alignment: Alignment.center,
child: Text(
transcribingText,
resolvedTranscribingText,
style: const TextStyle(color: AppColors.slate500),
),
);
@@ -236,7 +245,7 @@ class MessageComposer extends StatelessWidget {
return Align(
alignment: Alignment.center,
child: Text(
holdToSpeakText,
resolvedHoldToSpeakText,
style: const TextStyle(color: AppColors.slate500),
),
);
+1 -1
View File
@@ -91,7 +91,7 @@ class _ToastWidgetState extends State<_ToastWidget>
@override
Widget build(BuildContext context) {
final config = ToastTypeConfig.fromType(widget.type);
final config = ToastTypeConfig.fromType(context, widget.type);
return Positioned(
top: MediaQuery.of(context).padding.top + 12,
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import '../../../core/l10n/l10n.dart';
import 'toast_type.dart';
import '../../../core/theme/design_tokens.dart';
@@ -19,38 +20,41 @@ class ToastTypeConfig {
required this.icon,
});
static ToastTypeConfig fromType(ToastType type) => switch (type) {
ToastType.success => const ToastTypeConfig(
surfaceColor: AppColors.feedbackSuccessSurface,
borderColor: AppColors.feedbackSuccessBorder,
iconColor: AppColors.feedbackSuccessIcon,
textColor: AppColors.feedbackSuccessText,
label: '成功',
icon: Icons.check_circle_outline,
),
ToastType.warning => const ToastTypeConfig(
surfaceColor: AppColors.feedbackWarningSurface,
borderColor: AppColors.feedbackWarningBorder,
iconColor: AppColors.feedbackWarningIcon,
textColor: AppColors.feedbackWarningText,
label: '提醒',
icon: Icons.warning_amber_rounded,
),
ToastType.error => const ToastTypeConfig(
surfaceColor: AppColors.feedbackErrorSurface,
borderColor: AppColors.feedbackErrorBorder,
iconColor: AppColors.feedbackErrorIcon,
textColor: AppColors.feedbackErrorText,
label: '错误',
icon: Icons.error_outline,
),
ToastType.info => const ToastTypeConfig(
surfaceColor: AppColors.feedbackInfoSurface,
borderColor: AppColors.feedbackInfoBorder,
iconColor: AppColors.feedbackInfoIcon,
textColor: AppColors.feedbackInfoText,
label: '提示',
icon: Icons.info_outline,
),
};
static ToastTypeConfig fromType(BuildContext context, ToastType type) {
final l10n = context.l10n;
return switch (type) {
ToastType.success => ToastTypeConfig(
surfaceColor: AppColors.feedbackSuccessSurface,
borderColor: AppColors.feedbackSuccessBorder,
iconColor: AppColors.feedbackSuccessIcon,
textColor: AppColors.feedbackSuccessText,
label: l10n.toastLabelSuccess,
icon: Icons.check_circle_outline,
),
ToastType.warning => ToastTypeConfig(
surfaceColor: AppColors.feedbackWarningSurface,
borderColor: AppColors.feedbackWarningBorder,
iconColor: AppColors.feedbackWarningIcon,
textColor: AppColors.feedbackWarningText,
label: l10n.toastLabelWarning,
icon: Icons.warning_amber_rounded,
),
ToastType.error => ToastTypeConfig(
surfaceColor: AppColors.feedbackErrorSurface,
borderColor: AppColors.feedbackErrorBorder,
iconColor: AppColors.feedbackErrorIcon,
textColor: AppColors.feedbackErrorText,
label: l10n.toastLabelError,
icon: Icons.error_outline,
),
ToastType.info => ToastTypeConfig(
surfaceColor: AppColors.feedbackInfoSurface,
borderColor: AppColors.feedbackInfoBorder,
iconColor: AppColors.feedbackInfoIcon,
textColor: AppColors.feedbackInfoText,
label: l10n.toastLabelInfo,
icon: Icons.info_outline,
),
};
}
}