From 267f521de41c95c4e5c5c9c8792f267a32adc73e Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Fri, 31 Oct 2025 00:17:29 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E7=BF=BB=E8=AF=91):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E8=AF=AD=E8=A8=80=E9=80=89=E6=8B=A9=E5=99=A8=E4=B8=BA=E9=80=9A?= =?UTF-8?q?=E7=94=A8=E7=BB=84=E4=BB=B6=E5=B9=B6=E4=BC=98=E5=8C=96=E7=BF=BB?= =?UTF-8?q?=E8=AF=91=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增通用语言选择器组件,统一各模块的语言选择UI 添加自动检测翻译键并支持多语言显示 移除冗余代码并复用统一语言选择弹窗 优化图片翻译视图的布局和交互 --- lib/core/translations/language/en_us.dart | 4 + lib/core/translations/language/zh_cn.dart | 4 + .../common/widgets/language_selector_bar.dart | 172 ++++++++++++ .../picture_translation_controller.dart | 4 +- .../views/picture_translation_view.dart | 264 +++++++----------- .../text_translation_controller.dart | 186 ++---------- .../views/text_translation_view.dart | 88 +----- .../dialogs/language_selection_dialog.dart | 91 ++++-- .../translation/views/translation_view.dart | 209 ++------------ 9 files changed, 412 insertions(+), 610 deletions(-) create mode 100644 lib/modules/common/widgets/language_selector_bar.dart diff --git a/lib/core/translations/language/en_us.dart b/lib/core/translations/language/en_us.dart index bd4ed2520..70bb7a124 100644 --- a/lib/core/translations/language/en_us.dart +++ b/lib/core/translations/language/en_us.dart @@ -991,6 +991,10 @@ const Map enUS = { "me": "Me", //我 "counterPart": "Counterpart", //对方 + // 通用对话按钮与选项(用于统一语言选择弹窗等) + // 翻译键代表的中文文本内容:取消/确认/自动检测 + "autoDetect": "Auto Detect", + // 翻译历史页 "noTranslationHistory": "No translation history", "clearHistory": "Clear History", diff --git a/lib/core/translations/language/zh_cn.dart b/lib/core/translations/language/zh_cn.dart index ee4db0321..38847d7e7 100644 --- a/lib/core/translations/language/zh_cn.dart +++ b/lib/core/translations/language/zh_cn.dart @@ -911,6 +911,10 @@ const Map zhCN = { "me": "我", //我 "counterPart": "对方", //对方 + // 通用对话按钮与选项(用于统一语言选择弹窗等) + // 以下翻译键的中文文本内容如注释所示 + "autoDetect": "自动检测", // 自动检测 + // 翻译历史页 (translation_history_view.dart) "noTranslationHistory": "暂无历史记录", "clearHistory": "清除历史记录", diff --git a/lib/modules/common/widgets/language_selector_bar.dart b/lib/modules/common/widgets/language_selector_bar.dart new file mode 100644 index 000000000..495a01a00 --- /dev/null +++ b/lib/modules/common/widgets/language_selector_bar.dart @@ -0,0 +1,172 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../../../core/theme/app_colors.dart'; + +/// 通用语言选择栏组件 +/// - 统一源/目标语言胶囊样式与交换按钮样式 +/// - 通过函数读取语言显示文本,以便模块内保持最小改动 +/// - 文案默认走 .tr,本地化显示语言名称(翻译键代表的中文文本内容) +class LanguageSelectorBar extends StatelessWidget { + const LanguageSelectorBar({ + Key? key, + required this.sourceText, + required this.targetText, + required this.onTapSource, + required this.onTapTarget, + required this.onSwap, + this.translateLanguageNames = true, + this.fontSizeSp = 15, + this.sourceSuffix, + this.targetSuffix, + }) : super(key: key); + + /// 返回源语言显示文本(建议返回语言中文名或翻译键) + final String Function() sourceText; + + /// 返回目标语言显示文本(建议返回语言中文名或翻译键) + final String Function() targetText; + + /// 源语言点击事件(打开语言选择弹窗) + final VoidCallback onTapSource; + + /// 目标语言点击事件(打开语言选择弹窗) + final VoidCallback onTapTarget; + + /// 语言交换事件 + final VoidCallback onSwap; + + /// 是否将语言名称作为翻译键进行翻译(翻译键代表的中文文本内容) + final bool translateLanguageNames; + + /// 字号(sp单位),默认 15,与语音翻译一致 + final double fontSizeSp; + + /// 源语言后缀(可选),用于追加图标等 + final Widget? sourceSuffix; + + /// 目标语言后缀(可选),用于追加图标等 + final Widget? targetSuffix; + + @override + Widget build(BuildContext context) { + final isDarkMode = Get.isDarkMode; + final capsuleBg = + isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100]; + final textColor = isDarkMode ? Colors.white : Colors.black; + final swapBg = + isDarkMode ? AppColors.primary.withOpacity(0.2) : Colors.blue[50]; + final swapIconColor = isDarkMode ? AppColors.primary : Colors.blue; + + // 规范化特殊项的翻译键,例如:自动检测 -> autoDetect(翻译键代表的中文文本内容) + String _normalizeKey(String text) { + switch (text) { + case '自动检测': + return 'autoDetect'; + default: + return text; // 其他保持原值,由 .tr 决定是否命中 + } + } + + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // 源语言胶囊 + Expanded( + child: GestureDetector( + onTap: onTapSource, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), + decoration: BoxDecoration( + color: capsuleBg, + borderRadius: BorderRadius.circular(16.r), + ), + child: Obx(() { + final raw = sourceText(); + final text = translateLanguageNames ? _normalizeKey(raw) : raw; + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: Text( + translateLanguageNames ? text.tr : text, + style: TextStyle( + fontSize: fontSizeSp.sp, + fontWeight: FontWeight.w600, + color: textColor, + ), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), + ), + if (sourceSuffix != null) ...[ + SizedBox(width: 4.w), + sourceSuffix!, + ], + ], + ); + }), + ), + ), + ), + + // 语言互换按钮 + GestureDetector( + onTap: onSwap, + child: Container( + margin: EdgeInsets.symmetric(horizontal: 8.w), + padding: EdgeInsets.all(4.r), + decoration: BoxDecoration( + color: swapBg, + shape: BoxShape.circle, + ), + child: Icon( + Icons.swap_horiz, + color: swapIconColor, + size: 20.sp, + ), + ), + ), + + // 目标语言胶囊 + Expanded( + child: GestureDetector( + onTap: onTapTarget, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), + decoration: BoxDecoration( + color: capsuleBg, + borderRadius: BorderRadius.circular(16.r), + ), + child: Obx(() { + final raw = targetText(); + final text = translateLanguageNames ? _normalizeKey(raw) : raw; + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: Text( + translateLanguageNames ? text.tr : text, + style: TextStyle( + fontSize: fontSizeSp.sp, + fontWeight: FontWeight.w600, + color: textColor, + ), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), + ), + if (targetSuffix != null) ...[ + SizedBox(width: 4.w), + targetSuffix!, + ], + ], + ); + }), + ), + ), + ), + ], + ); + } +} diff --git a/lib/modules/picture_translation/controllers/picture_translation_controller.dart b/lib/modules/picture_translation/controllers/picture_translation_controller.dart index 52684ab48..9f25c0350 100644 --- a/lib/modules/picture_translation/controllers/picture_translation_controller.dart +++ b/lib/modules/picture_translation/controllers/picture_translation_controller.dart @@ -137,14 +137,14 @@ class PictureTranslationController extends GetxController { void deleteHistory(String id) { historyList.removeWhere((item) => item.id == id); _saveHistory(); - Get.snackbar('成功', '已删除历史记录'); + // Get.snackbar('成功', '已删除历史记录'); } /// 清空历史记录 void clearHistory() { historyList.clear(); _storage.remove(_historyKey); - Get.snackbar('成功', '已清空所有历史记录'); + // Get.snackbar('成功', '已清空所有历史记录'); } /// 检查权限 diff --git a/lib/modules/picture_translation/views/picture_translation_view.dart b/lib/modules/picture_translation/views/picture_translation_view.dart index 9c26f50e5..ff455d9b2 100644 --- a/lib/modules/picture_translation/views/picture_translation_view.dart +++ b/lib/modules/picture_translation/views/picture_translation_view.dart @@ -7,9 +7,12 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:image_picker/image_picker.dart'; import 'package:photo_view/photo_view.dart'; // 新增:图片查看器 +import '../../../core/theme/app_colors.dart'; import '../controllers/picture_translation_controller.dart'; import '../models/picture_translation_models.dart'; import 'picture_translation_history_detail_view.dart'; +import '../../translation/views/dialogs/language_selection_dialog.dart'; +import '../../common/widgets/language_selector_bar.dart'; class PictureTranslationView extends GetView { const PictureTranslationView({super.key}); @@ -30,8 +33,6 @@ class PictureTranslationView extends GetView { delegate: SliverChildListDelegate([ _buildQuickActions(), SizedBox(height: 16.h), - _buildLanguageSelector(), - SizedBox(height: 16.h), _buildImageSection(), SizedBox(height: 16.h), if (controller.currentStep.value.status != @@ -57,10 +58,12 @@ class PictureTranslationView extends GetView { /// 构建应用栏 PreferredSizeWidget _buildAppBar(bool isDarkMode) { return AppBar( + backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white, + elevation: 1, leading: IconButton( icon: Icon( Icons.arrow_back_ios, - size: 20.w, + size: 20.sp, color: isDarkMode ? Colors.white : Colors.black87, ), onPressed: () => Get.back(), @@ -73,42 +76,62 @@ class PictureTranslationView extends GetView { fontWeight: FontWeight.w600, color: isDarkMode ? Colors.white : Colors.black, ), + overflow: TextOverflow.ellipsis, + maxLines: 1, ), - backgroundColor: isDarkMode ? Colors.black : Colors.white, foregroundColor: isDarkMode ? Colors.white : Colors.black87, - elevation: 0, actions: [ IconButton( icon: const Icon(Icons.history), onPressed: _showHistoryBottomSheet, - tooltip: '历史记录', + tooltip: 'historyRecords'.tr, //历史记录 ), PopupMenuButton( onSelected: _handleMenuAction, itemBuilder: (context) => [ - const PopupMenuItem( + PopupMenuItem( value: 'clear', child: Row( children: [ - Icon(Icons.clear_all, size: 20), - SizedBox(width: 8), - Text('清空结果'), + const Icon(Icons.clear_all, size: 20), + const SizedBox(width: 8), + Text('clearResults'.tr), //清空结果 ], ), ), - const PopupMenuItem( + PopupMenuItem( value: 'settings', child: Row( children: [ - Icon(Icons.settings, size: 20), - SizedBox(width: 8), - Text('设置'), + const Icon(Icons.settings, size: 20), + const SizedBox(width: 8), + Text('settings'.tr), //设置 ], ), ), ], ), ], + bottom: PreferredSize( + preferredSize: Size.fromHeight(50.h), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), + child: _buildLanguageSelector(isDarkMode), + ), + ), + ); + } + + /// 构建语言选择栏 + Widget _buildLanguageSelector(bool isDarkMode) { + return LanguageSelectorBar( + sourceText: () => controller.sourceLanguageChineseName.value, + targetText: () => controller.targetLanguageChineseName.value, + onTapSource: () => _showLanguageDialog(true), + onTapTarget: () => _showLanguageDialog(false), + onSwap: controller.swapLanguages, + translateLanguageNames: true, // 翻译键代表的中文文本内容 + fontSizeSp: 15, ); } @@ -131,7 +154,7 @@ class PictureTranslationView extends GetView { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '选择图片来源', + '选择图片来源'.tr, //选择图片来源 style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, @@ -144,7 +167,7 @@ class PictureTranslationView extends GetView { Expanded( child: _buildActionButton( icon: Icons.photo_library, - label: '相册', + label: 'photoAlbum'.tr, onTap: controller.pickFromGallery, color: Colors.blue, ), @@ -153,7 +176,7 @@ class PictureTranslationView extends GetView { Expanded( child: _buildActionButton( icon: Icons.camera_alt, - label: '拍照', + label: 'takePhoto'.tr, onTap: controller.pickFromCamera, color: Colors.green, ), @@ -203,128 +226,29 @@ class PictureTranslationView extends GetView { ); } - /// 构建语言选择器 - Widget _buildLanguageSelector() { - return Container( - padding: EdgeInsets.all(16.w), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '语言设置', - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: Colors.black87, - ), - ), - SizedBox(height: 12.h), - Row( - children: [ - Expanded( - child: _buildLangTile( - '源语言', - controller.sourceLanguageChineseName.value, - () => _showLangDialog(true), - Colors.blue, - ), - ), - SizedBox(width: 8.w), - IconButton( - onPressed: controller.swapLanguages, - icon: const Icon(Icons.swap_horiz), - tooltip: '交换语言', - ), - SizedBox(width: 8.w), - Expanded( - child: _buildLangTile( - '目标语言', - controller.targetLanguageChineseName.value, - () => _showLangDialog(false), - Colors.green, - ), - ), - ], - ), - ], - ), - ); - } - - Widget _buildLangTile( - String label, String value, VoidCallback onTap, Color color) { - return InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(8.r), - child: Container( - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8.r), - border: Border.all(color: color.withOpacity(0.3)), - color: color.withOpacity(0.05), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: TextStyle(fontSize: 12.sp, color: Colors.grey[600]), - ), - SizedBox(height: 4.h), - Text( - value, - style: TextStyle( - fontSize: 14.sp, - fontWeight: FontWeight.w600, - color: color, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - ); - } - - Future _showLangDialog(bool isSource) async { + Future _showLanguageDialog(bool isSource) async { + final isDarkMode = Get.isDarkMode; // 使用Get.isDarkMode替代Theme.of(context) final langs = controller.languageManager.getAllChineseNames(); - final selected = await Get.dialog( - SimpleDialog( - title: Text(isSource ? '选择源语言' : '选择目标语言'), - children: [ - SizedBox( - width: 320.w, - height: 420.h, - child: ListView.builder( - itemCount: langs.length, - itemBuilder: (_, i) => ListTile( - title: Text(langs[i]), - onTap: () => Get.back(result: langs[i]), - ), - ), - ) - ], - ), + final currentLanguage = isSource + ? controller.sourceLanguageChineseName.value + : controller.targetLanguageChineseName.value; + + // 复用统一语言选择弹窗组件(保持中文名直接显示) + LanguageSelectionDialog.show( + isSource, + isDarkMode, + languages: langs, + currentLanguage: currentLanguage, + includeAutoDetect: true, // 图片翻译也支持自动检测 + translateLanguageNames: true, // 开启多语言显示,语言中文名作为翻译键 + onSelected: (name) { + if (isSource) { + controller.setSourceLanguage(name); + } else { + controller.setTargetLanguage(name); + } + }, ); - if (selected != null) { - if (isSource) { - controller.setSourceLanguage(selected); - } else { - controller.setTargetLanguage(selected); - } - } } Widget _buildImageSection() { @@ -577,7 +501,7 @@ class PictureTranslationView extends GetView { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - '$title ($language)', + '$title (${language.tr})', style: TextStyle( fontSize: 14.sp, fontWeight: FontWeight.w600, @@ -588,11 +512,11 @@ class PictureTranslationView extends GetView { icon: Icon(Icons.copy, size: 18.sp, color: color), onPressed: () { Clipboard.setData(ClipboardData(text: content)); - Get.snackbar('成功', '已复制到剪切板'); + Get.snackbar('success'.tr, 'copied'.tr); // 翻译键代表的中文文本内容 }, padding: EdgeInsets.zero, constraints: const BoxConstraints(), - tooltip: '复制', + tooltip: 'copy'.tr, // 翻译键代表的中文文本内容 ), ], ), @@ -629,7 +553,7 @@ class PictureTranslationView extends GetView { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - '最近翻译', + 'translationHistory'.tr, // 翻译键代表的中文文本内容 style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, @@ -638,7 +562,7 @@ class PictureTranslationView extends GetView { ), TextButton( onPressed: _showHistoryBottomSheet, - child: const Text('查看全部'), + child: Text('viewAll'.tr), // 翻译键代表的中文文本内容 ), ], ), @@ -648,7 +572,7 @@ class PictureTranslationView extends GetView { padding: EdgeInsets.symmetric(horizontal: 16.w).copyWith(bottom: 16.h), child: Text( - '暂无翻译记录', + 'noTranslationHistory'.tr, // 翻译键代表的中文文本内容 style: TextStyle(fontSize: 14.sp, color: Colors.grey[600]), ), ) @@ -707,7 +631,7 @@ class PictureTranslationView extends GetView { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - '翻译历史', + 'translationHistory'.tr, // 翻译键代表的中文文本内容 style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold), ), @@ -724,7 +648,7 @@ class PictureTranslationView extends GetView { if (controller.historyList.isEmpty) { return Center( child: Text( - '暂无翻译记录', + 'noTranslationHistory'.tr, // 翻译键代表的中文文本内容 style: TextStyle(fontSize: 16.sp, color: Colors.grey[600]), ), @@ -924,29 +848,33 @@ class PictureTranslationView extends GetView { ], ), ) - : ClipRRect( - borderRadius: BorderRadius.circular(8.r), - child: Image.file( - file, - fit: BoxFit.cover, - width: double.infinity, - errorBuilder: (context, error, stackTrace) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.error_outline, - size: 48.sp, color: Colors.red[400]), - SizedBox(height: 8.h), - Text( - '图片加载失败', - style: TextStyle( - fontSize: 14.sp, color: Colors.red[600]), - ), - ], - ), - ); - }, + : GestureDetector( + // 点击原图预览打开查看器(与翻译后的图片预览一致) + onTap: () => _openImageViewer(FileImage(file)), + child: ClipRRect( + borderRadius: BorderRadius.circular(8.r), + child: Image.file( + file, + fit: BoxFit.cover, + width: double.infinity, + errorBuilder: (context, error, stackTrace) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error_outline, + size: 48.sp, color: Colors.red[400]), + SizedBox(height: 8.h), + Text( + '图片加载失败', + style: TextStyle( + fontSize: 14.sp, color: Colors.grey[600]), + ), + ], + ), + ); + }, + ), ), ), ); diff --git a/lib/modules/text_translation/controllers/text_translation_controller.dart b/lib/modules/text_translation/controllers/text_translation_controller.dart index e6f4588e7..8b4b1ed5e 100644 --- a/lib/modules/text_translation/controllers/text_translation_controller.dart +++ b/lib/modules/text_translation/controllers/text_translation_controller.dart @@ -7,6 +7,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../../../data/services/volcano_translation_service.dart'; import '../../../data/services/language_manager.dart'; import '../../../core/utils/logger.dart'; +import '../../translation/views/dialogs/language_selection_dialog.dart'; /// 文本翻译控制器 class TextTranslationController extends GetxController { @@ -131,7 +132,7 @@ class TextTranslationController extends GetxController { sourceLanguageChineseName.value = language; // 同时更新语言代码;当选择“自动检测”时,将源语言代码置空,后续翻译流程会进行语种检测 - if (language == '自动检测') { + if (language == 'autoDetect') { sourceLanguageCode.value = ''; } else { final asrCode = _languageManager.getAsrCodeByChineseName(language); @@ -248,7 +249,7 @@ class TextTranslationController extends GetxController { Logger.info('调用翻译服务...'); // 若选择了“自动检测”,先进行语种检测 - if (sourceLanguageChineseName.value == '自动检测' || + if (sourceLanguageChineseName.value == 'autoDetect' || sourceLanguageCode.value.isEmpty) { Logger.info('源语言为自动检测,开始语种检测…'); final detectedAsrCode = @@ -256,8 +257,8 @@ class TextTranslationController extends GetxController { if (detectedAsrCode == null || detectedAsrCode.isEmpty) { Logger.error('语种检测失败'); Get.snackbar( - '错误', // 翻译键代表的中文文本内容 - '语种检测失败,请手动选择源语言或稍后重试', // 翻译键代表的中文文本内容 + 'error'.tr, // 翻译键代表的中文文本内容 + 'languageDetectionFailed'.tr, // 翻译键代表的中文文本内容 snackPosition: SnackPosition.TOP, backgroundColor: Colors.red.withOpacity(0.9), colorText: Colors.white, @@ -322,164 +323,23 @@ class TextTranslationController extends GetxController { /// 显示语言选择对话框(统一与其它翻译模式的样式) void showLanguageSelector({required bool isSourceLanguage}) { final isDarkMode = Theme.of(Get.context!).brightness == Brightness.dark; - - Get.dialog(Dialog( - backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16.r), // 统一圆角尺寸 - ), - alignment: - isSourceLanguage ? Alignment.centerLeft : Alignment.centerRight, - insetPadding: EdgeInsets.symmetric(horizontal: 10.w), - child: SizedBox( - width: 0.4.sw, - child: Padding( - padding: EdgeInsets.all(12.w), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 标题区域 - Padding( - padding: EdgeInsets.symmetric(horizontal: 8.w), - child: Text( - isSourceLanguage - ? 'selectSourceLanguage'.tr // 翻译键代表的中文文本内容 - : 'selectTargetLanguage'.tr, // 翻译键代表的中文文本内容 - style: TextStyle( - fontSize: 18.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black87, - ), - ), - ), - SizedBox(height: 16.h), - - // 语言列表区域 - Container( - constraints: BoxConstraints(maxHeight: 0.5.sh), - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: List.generate( - isSourceLanguage - ? (supportedLanguages.length + 1) - : supportedLanguages.length, - (index) { - // 源语言下首项为“自动检测” - final displayText = isSourceLanguage && index == 0 - ? 'autoDetect'.tr // 翻译键代表的中文文本内容 - : (isSourceLanguage - ? supportedLanguages[index - 1] - : supportedLanguages[index]); - final valueText = isSourceLanguage && index == 0 - ? '自动检测' - : (isSourceLanguage - ? supportedLanguages[index - 1] - : supportedLanguages[index]); - - final isSelected = isSourceLanguage - ? valueText == sourceLanguageChineseName.value - : valueText == targetLanguageChineseName.value; - - return GestureDetector( - onTap: () { - if (isSourceLanguage) { - setSourceLanguage(valueText); - } else { - setTargetLanguage(valueText); - } - Get.back(); - }, - child: Container( - margin: EdgeInsets.only(bottom: 10.h), - padding: EdgeInsets.all(10.w), - decoration: BoxDecoration( - color: isSelected - ? (isDarkMode - ? const Color(0xFF6200EE).withOpacity(0.2) - : Colors.blue.withOpacity(0.1)) - : (isDarkMode - ? Colors.white.withOpacity(0.05) - : Colors.grey[200]!), - borderRadius: BorderRadius.circular(12.r), - border: Border.all( - color: isSelected - ? (isDarkMode - ? const Color(0xFF6200EE) - : Colors.blue) - : (isDarkMode - ? Colors.white.withOpacity(0.05) - : Colors.grey[200]!), - width: 1, - ), - ), - child: Row( - children: [ - // 语言名称(中文名直接显示不进行翻译) - Expanded( - child: Text( - displayText, - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w500, - color: isDarkMode - ? Colors.white - : Colors.black87, - overflow: TextOverflow.ellipsis, - ), - ), - ), - // 选中指示器 - Container( - width: 24.w, - height: 24.w, - decoration: BoxDecoration( - color: isSelected - ? (isDarkMode - ? const Color(0xFF6200EE) - : Colors.blue) - : (isDarkMode - ? Colors.white.withOpacity(0.1) - : Colors.grey[300]), - shape: BoxShape.circle, - ), - child: isSelected - ? Icon( - Icons.check, - size: 16.w, - color: Colors.white, - ) - : null, - ), - ], - ), - ), - ); - }, - ), - ), - ), - ), - SizedBox(height: 16.h), - - // 取消按钮 - Align( - alignment: Alignment.centerRight, - child: TextButton( - onPressed: () => Get.back(), - child: Text( - 'cancel'.tr, // 翻译键代表的中文文本内容 - style: TextStyle( - color: isDarkMode ? const Color(0xFF6200EE) : Colors.blue, - ), - ), - ), - ), - ], - ), - ), - ), - )); + // 复用统一语言选择弹窗组件 + LanguageSelectionDialog.show( + isSourceLanguage, + isDarkMode, + languages: supportedLanguages, + currentLanguage: isSourceLanguage + ? sourceLanguageChineseName.value + : targetLanguageChineseName.value, + includeAutoDetect: true, // 文本翻译源语言保留“自动检测” + translateLanguageNames: true, // 开启多语言显示,语言中文名作为翻译键 + onSelected: (name) { + if (isSourceLanguage) { + setSourceLanguage(name); + } else { + setTargetLanguage(name); + } + }, + ); } } diff --git a/lib/modules/text_translation/views/text_translation_view.dart b/lib/modules/text_translation/views/text_translation_view.dart index 0722bb111..723e3cd48 100644 --- a/lib/modules/text_translation/views/text_translation_view.dart +++ b/lib/modules/text_translation/views/text_translation_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../../../core/theme/app_colors.dart'; +import '../../common/widgets/language_selector_bar.dart'; import '../controllers/text_translation_controller.dart'; @@ -93,85 +94,16 @@ class TextTranslationView extends GetView { ); } - /// 构建语言选择器(统一与其它翻译模式的样式) + /// 构建语言选择器(通用组件) Widget _buildLanguageSelector(bool isDarkMode) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // 源语言选择(与面对面模式一致的胶囊样式) - Expanded( - child: GestureDetector( - onTap: () => - controller.showLanguageSelector(isSourceLanguage: true), - child: Container( - padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), - decoration: BoxDecoration( - color: isDarkMode - ? Colors.white.withOpacity(0.1) - : Colors.grey[100], - borderRadius: BorderRadius.circular(16.r), - ), - child: Obx(() => Text( - controller.sourceLanguageChineseName.value, - style: TextStyle( - fontSize: 15.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black, - ), - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - )), - ), - ), - ), - - // 语言互换按钮(统一样式与颜色) - GestureDetector( - onTap: controller.swapLanguages, - child: Container( - margin: EdgeInsets.symmetric(horizontal: 8.w), - padding: EdgeInsets.all(4.r), - decoration: BoxDecoration( - color: isDarkMode - ? AppColors.primary.withOpacity(0.2) - : Colors.blue[50], - shape: BoxShape.circle, - ), - child: Icon( - Icons.swap_horiz, - color: isDarkMode ? AppColors.primary : Colors.blue, - size: 20.sp, - ), - ), - ), - - // 目标语言选择(与面对面模式一致的胶囊样式) - Expanded( - child: GestureDetector( - onTap: () => - controller.showLanguageSelector(isSourceLanguage: false), - child: Container( - padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), - decoration: BoxDecoration( - color: isDarkMode - ? Colors.white.withOpacity(0.1) - : Colors.grey[100], - borderRadius: BorderRadius.circular(16.r), - ), - child: Obx(() => Text( - controller.targetLanguageChineseName.value, - style: TextStyle( - fontSize: 15.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black, - ), - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - )), - ), - ), - ), - ], + return LanguageSelectorBar( + sourceText: () => controller.sourceLanguageChineseName.value, + targetText: () => controller.targetLanguageChineseName.value, + onTapSource: () => controller.showLanguageSelector(isSourceLanguage: true), + onTapTarget: () => controller.showLanguageSelector(isSourceLanguage: false), + onSwap: controller.swapLanguages, + translateLanguageNames: true, // 翻译键代表的中文文本内容 + fontSizeSp: 15, ); } diff --git a/lib/modules/translation/views/dialogs/language_selection_dialog.dart b/lib/modules/translation/views/dialogs/language_selection_dialog.dart index cbd889f5c..6bd5be25d 100644 --- a/lib/modules/translation/views/dialogs/language_selection_dialog.dart +++ b/lib/modules/translation/views/dialogs/language_selection_dialog.dart @@ -5,10 +5,45 @@ import '../../controllers/translation_controller.dart'; /// 语言选择对话框 class LanguageSelectionDialog { - /// 显示语言选择对话框 - static void show(bool isSource, bool isDarkMode) { - final controller = Get.find(); - + /// 显示语言选择对话框(通用,支持文本/图片/语音翻译) + /// [isSource] 是否为源语言选择 + /// [isDarkMode] 深色模式标记 + /// [languages] 可选,自定义语言列表(中文名称列表);若为空则使用语音翻译模块的列表 + /// [currentLanguage] 当前选中的语言中文名称 + /// [includeAutoDetect] 是否在源语言首项加入“自动检测”选项(仅文本翻译需要) + /// [translateLanguageNames] 是否将语言名称作为翻译键进行翻译(语音翻译保持原逻辑) + /// [onSelected] 选中项回调,参数为中文名称 + static void show( + bool isSource, + bool isDarkMode, { + List? languages, + String? currentLanguage, + bool includeAutoDetect = false, + bool translateLanguageNames = true, + ValueChanged? onSelected, + }) { + // 若未提供自定义列表,则在已注册时回退到语音翻译控制器的列表 + final hasTranslationController = Get.isRegistered(); + final TranslationController? controller = + hasTranslationController ? Get.find() : null; + final fallbackLanguages = + controller?.supportedLanguages.keys.toList() ?? []; // 中文名称列表 + + final displayLanguages = List.from(languages ?? fallbackLanguages); + + // 源语言是否展示“自动检测”选项(仅列表显示,值为中文“自动检测”) + final hasAutoDetect = includeAutoDetect && isSource; + + // 计算当前选中项(优先传入值,其次回退到语音翻译控制器,最后列表首项或自动检测) + final selectedLanguage = currentLanguage ?? + (controller != null + ? (isSource + ? controller.sourceLanguage.value + : controller.targetLanguage.value) + : (hasAutoDetect + ? 'autoDetect' + : (displayLanguages.isNotEmpty ? displayLanguages.first : ''))); + Get.dialog(Dialog( backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, shape: RoundedRectangleBorder( @@ -30,8 +65,8 @@ class LanguageSelectionDialog { padding: EdgeInsets.symmetric(horizontal: 8.w), child: Text( isSource - ? 'selectSourceLanguage'.tr - : 'selectTargetLanguage'.tr, // 保留源语言/目标语言区分 + ? 'selectSourceLanguage'.tr // 翻译键代表的中文文本内容 + : 'selectTargetLanguage'.tr, // 翻译键代表的中文文本内容 style: TextStyle( fontSize: 18.sp, // 调整字号 fontWeight: FontWeight.w600, // 使用数字字重 @@ -47,21 +82,37 @@ class LanguageSelectionDialog { child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, - children: - controller.supportedLanguages.keys.map((language) { - return Obx(() { - final currentLanguage = isSource - ? controller.sourceLanguage.value - : controller.targetLanguage.value; - final isSelected = currentLanguage == language; + children: List.generate( + hasAutoDetect + ? (displayLanguages.length + 1) + : displayLanguages.length, + (index) { + // 构造展示与实际值 + final isAutoDetectItem = hasAutoDetect && index == 0; + final valueText = isAutoDetectItem + ? 'autoDetect'.tr + : displayLanguages[ + hasAutoDetect ? index - 1 : index]; + final displayText = isAutoDetectItem + ? 'autoDetect'.tr // 翻译键代表的中文文本内容 + : (translateLanguageNames + ? valueText.tr + : valueText); + + final isSelected = selectedLanguage == valueText; return GestureDetector( // 改用手势检测器实现点击 onTap: () { - if (isSource) { - controller.setSourceLanguage(language); + if (onSelected != null) { + onSelected(valueText); } else { - controller.setTargetLanguage(language); + // 默认行为:语音翻译控制器设置语言 + if (isSource) { + controller?.setSourceLanguage(valueText); + } else { + controller?.setTargetLanguage(valueText); + } } Get.back(); }, @@ -94,7 +145,7 @@ class LanguageSelectionDialog { // 语言名称 Expanded( child: Text( - language.tr, + displayText, style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w500, @@ -132,8 +183,8 @@ class LanguageSelectionDialog { ), ), ); - }); - }).toList(), + }, + ), ), ), ), @@ -145,7 +196,7 @@ class LanguageSelectionDialog { child: TextButton( onPressed: () => Get.back(), child: Text( - 'cancel'.tr, + 'cancel'.tr, // 翻译键代表的中文文本内容 style: TextStyle( color: isDarkMode ? const Color(0xFF6200EE) : Colors.blue, ), diff --git a/lib/modules/translation/views/translation_view.dart b/lib/modules/translation/views/translation_view.dart index dc49b1770..39a68f818 100644 --- a/lib/modules/translation/views/translation_view.dart +++ b/lib/modules/translation/views/translation_view.dart @@ -12,6 +12,7 @@ import '../views/translation_history_view.dart'; import '../bindings/translation_history_binding.dart'; import '../../../core/theme/app_colors.dart'; import '../../../routes/app_routes.dart'; +import '../../common/widgets/language_selector_bar.dart'; // 导入拆分后的组件 import 'widgets/face_to_face_layout.dart'; // 面对面翻译布局 @@ -268,108 +269,28 @@ class TranslationView extends GetView { child: Column( mainAxisSize: MainAxisSize.min, children: [ - // 原语言选择行 - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // 源语言选择 - Expanded( - child: GestureDetector( - onTap: () => - LanguageSelectionDialog.show(true, isDarkMode), - child: Container( - padding: EdgeInsets.symmetric( - horizontal: 8.w, vertical: 4.h), - decoration: BoxDecoration( - color: isDarkMode - ? Colors.white.withOpacity(0.1) - : Colors.grey[100], - borderRadius: BorderRadius.circular(16.r), - ), - child: Obx(() => Text( - '${controller.sourceLanguage.value.split(' ')[0]}' - .tr, - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black, - ), - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - )), - ), - ), - ), - - // 语言互换按钮 - GestureDetector( - onTap: controller.swapLanguages, - child: Container( - margin: EdgeInsets.symmetric(horizontal: 8.w), - padding: EdgeInsets.all(4.r), - decoration: BoxDecoration( - color: isDarkMode - ? AppColors.primary.withOpacity(0.2) - : Colors.blue[50], - shape: BoxShape.circle, - ), - child: Icon( - Icons.swap_horiz, - color: isDarkMode ? AppColors.primary : Colors.blue, + // 统一通用语言选择栏组件 + LanguageSelectorBar( + sourceText: () => + controller.sourceLanguage.value.split(' ')[0], + targetText: () => + controller.targetLanguage.value.split(' ')[0], + onTapSource: () => + LanguageSelectionDialog.show(true, isDarkMode), + onTapTarget: () => + LanguageSelectionDialog.show(false, isDarkMode), + onSwap: controller.swapLanguages, + translateLanguageNames: true, // 翻译键代表的中文文本内容 + fontSizeSp: 15, + targetSuffix: controller.currentMode.value == 'call' + ? Icon( + Icons.phone_in_talk, size: 20.sp, - ), - ), - ), - - // 目标语言选择 - Expanded( - child: GestureDetector( - onTap: () => - LanguageSelectionDialog.show(false, isDarkMode), - child: Container( - padding: EdgeInsets.symmetric( - horizontal: 8.w, vertical: 4.h), - decoration: BoxDecoration( - color: isDarkMode - ? Colors.white.withOpacity(0.1) - : Colors.grey[100], - borderRadius: BorderRadius.circular(16.r), - ), - child: Obx(() => Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Flexible( - child: Text( - '${controller.targetLanguage.value.split(' ')[0]}' - .tr, - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: isDarkMode - ? Colors.white - : Colors.black, - ), - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - ), - ), - //只在通话翻译模式下显示电话图标 - if (controller.currentMode.value == 'call') ...[ - SizedBox(width: 4.w), - Icon( - Icons.phone_in_talk, - size: 20.sp, - color: isDarkMode - ? Colors.white70 - : Colors.grey[600], - ), - ], - ], - )), - ), - ), - ), - ], + color: isDarkMode + ? Colors.white70 + : Colors.grey[600], + ) + : null, ), // 通话模式下的“对方 / 我”标签行 @@ -1182,84 +1103,14 @@ class TranslationView extends GetView { ), ], ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // 源语言选择 - Expanded( - child: GestureDetector( - onTap: () => LanguageSelectionDialog.show(true, isDarkMode), - child: Container( - padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), - decoration: BoxDecoration( - color: isDarkMode - ? Colors.white.withOpacity(0.1) - : Colors.grey[100], - borderRadius: BorderRadius.circular(16.r), - ), - child: Text( - '${controller.sourceLanguage.value.split(' ')[0]}'.tr, - style: TextStyle( - fontSize: 15.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black, - ), - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - ), - ), - ), - ), - - // 语言互换按钮 - GestureDetector( - onTap: controller.swapLanguages, - child: Container( - margin: EdgeInsets.symmetric(horizontal: 8.w), - padding: EdgeInsets.all(4.r), - decoration: BoxDecoration( - color: isDarkMode - ? AppColors.primary.withOpacity(0.2) - : Colors.blue[50], - shape: BoxShape.circle, - ), - child: Icon( - Icons.swap_horiz, - color: isDarkMode ? AppColors.primary : Colors.blue, - size: 20.sp, - ), - ), - ), - - // 目标语言选择 - Expanded( - child: GestureDetector( - onTap: () => LanguageSelectionDialog.show(false, isDarkMode), - child: Container( - padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), - decoration: BoxDecoration( - color: isDarkMode - ? Colors.white.withOpacity(0.1) - : Colors.grey[100], - borderRadius: BorderRadius.circular(16.r), - ), - child: Transform.rotate( - angle: 3.14159, // 旋转180度 - child: Text( - '${controller.targetLanguage.value.split(' ')[0]}'.tr, - style: TextStyle( - fontSize: 15.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black, - ), - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - ), - ), - ), - ), - ), - ], + child: LanguageSelectorBar( + sourceText: () => controller.sourceLanguage.value.split(' ')[0], + targetText: () => controller.targetLanguage.value.split(' ')[0], + onTapSource: () => LanguageSelectionDialog.show(true, isDarkMode), + onTapTarget: () => LanguageSelectionDialog.show(false, isDarkMode), + onSwap: controller.swapLanguages, + translateLanguageNames: true, // 翻译键代表的中文文本内容 + fontSizeSp: 15, ), ); }