Browse Source

feat(翻译): 重构语言选择器为通用组件并优化翻译功能

新增通用语言选择器组件,统一各模块的语言选择UI
添加自动检测翻译键并支持多语言显示
移除冗余代码并复用统一语言选择弹窗
优化图片翻译视图的布局和交互
weicu
lxm 12 months ago
parent
commit
267f521de4
  1. 4
      lib/core/translations/language/en_us.dart
  2. 4
      lib/core/translations/language/zh_cn.dart
  3. 172
      lib/modules/common/widgets/language_selector_bar.dart
  4. 4
      lib/modules/picture_translation/controllers/picture_translation_controller.dart
  5. 264
      lib/modules/picture_translation/views/picture_translation_view.dart
  6. 186
      lib/modules/text_translation/controllers/text_translation_controller.dart
  7. 88
      lib/modules/text_translation/views/text_translation_view.dart
  8. 91
      lib/modules/translation/views/dialogs/language_selection_dialog.dart
  9. 209
      lib/modules/translation/views/translation_view.dart

4
lib/core/translations/language/en_us.dart

@ -991,6 +991,10 @@ const Map<String, String> enUS = {
"me": "Me", //我 "me": "Me", //我
"counterPart": "Counterpart", //对方 "counterPart": "Counterpart", //对方
// 通用对话按钮与选项(用于统一语言选择弹窗等)
// 翻译键代表的中文文本内容:取消/确认/自动检测
"autoDetect": "Auto Detect",
// 翻译历史页 // 翻译历史页
"noTranslationHistory": "No translation history", "noTranslationHistory": "No translation history",
"clearHistory": "Clear History", "clearHistory": "Clear History",

4
lib/core/translations/language/zh_cn.dart

@ -911,6 +911,10 @@ const Map<String, String> zhCN = {
"me": "我", //我 "me": "我", //我
"counterPart": "对方", //对方 "counterPart": "对方", //对方
// 通用对话按钮与选项(用于统一语言选择弹窗等)
// 以下翻译键的中文文本内容如注释所示
"autoDetect": "自动检测", // 自动检测
// 翻译历史页 (translation_history_view.dart) // 翻译历史页 (translation_history_view.dart)
"noTranslationHistory": "暂无历史记录", "noTranslationHistory": "暂无历史记录",
"clearHistory": "清除历史记录", "clearHistory": "清除历史记录",

172
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!,
],
],
);
}),
),
),
),
],
);
}
}

4
lib/modules/picture_translation/controllers/picture_translation_controller.dart

@ -137,14 +137,14 @@ class PictureTranslationController extends GetxController {
void deleteHistory(String id) { void deleteHistory(String id) {
historyList.removeWhere((item) => item.id == id); historyList.removeWhere((item) => item.id == id);
_saveHistory(); _saveHistory();
Get.snackbar('成功', '已删除历史记录'); // Get.snackbar('成功', '已删除历史记录');
} }
/// 清空历史记录 /// 清空历史记录
void clearHistory() { void clearHistory() {
historyList.clear(); historyList.clear();
_storage.remove(_historyKey); _storage.remove(_historyKey);
Get.snackbar('成功', '已清空所有历史记录'); // Get.snackbar('成功', '已清空所有历史记录');
} }
/// 检查权限 /// 检查权限

264
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:image_picker/image_picker.dart';
import 'package:photo_view/photo_view.dart'; // 新增:图片查看器 import 'package:photo_view/photo_view.dart'; // 新增:图片查看器
import '../../../core/theme/app_colors.dart';
import '../controllers/picture_translation_controller.dart'; import '../controllers/picture_translation_controller.dart';
import '../models/picture_translation_models.dart'; import '../models/picture_translation_models.dart';
import 'picture_translation_history_detail_view.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<PictureTranslationController> { class PictureTranslationView extends GetView<PictureTranslationController> {
const PictureTranslationView({super.key}); const PictureTranslationView({super.key});
@ -30,8 +33,6 @@ class PictureTranslationView extends GetView<PictureTranslationController> {
delegate: SliverChildListDelegate([ delegate: SliverChildListDelegate([
_buildQuickActions(), _buildQuickActions(),
SizedBox(height: 16.h), SizedBox(height: 16.h),
_buildLanguageSelector(),
SizedBox(height: 16.h),
_buildImageSection(), _buildImageSection(),
SizedBox(height: 16.h), SizedBox(height: 16.h),
if (controller.currentStep.value.status != if (controller.currentStep.value.status !=
@ -57,10 +58,12 @@ class PictureTranslationView extends GetView<PictureTranslationController> {
/// 构建应用栏 /// 构建应用栏
PreferredSizeWidget _buildAppBar(bool isDarkMode) { PreferredSizeWidget _buildAppBar(bool isDarkMode) {
return AppBar( return AppBar(
backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white,
elevation: 1,
leading: IconButton( leading: IconButton(
icon: Icon( icon: Icon(
Icons.arrow_back_ios, Icons.arrow_back_ios,
size: 20.w, size: 20.sp,
color: isDarkMode ? Colors.white : Colors.black87, color: isDarkMode ? Colors.white : Colors.black87,
), ),
onPressed: () => Get.back(), onPressed: () => Get.back(),
@ -73,42 +76,62 @@ class PictureTranslationView extends GetView<PictureTranslationController> {
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black, color: isDarkMode ? Colors.white : Colors.black,
), ),
overflow: TextOverflow.ellipsis,
maxLines: 1,
), ),
backgroundColor: isDarkMode ? Colors.black : Colors.white,
foregroundColor: isDarkMode ? Colors.white : Colors.black87, foregroundColor: isDarkMode ? Colors.white : Colors.black87,
elevation: 0,
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Icons.history), icon: const Icon(Icons.history),
onPressed: _showHistoryBottomSheet, onPressed: _showHistoryBottomSheet,
tooltip: '历史记录', tooltip: 'historyRecords'.tr, //历史记录
), ),
PopupMenuButton<String>( PopupMenuButton<String>(
onSelected: _handleMenuAction, onSelected: _handleMenuAction,
itemBuilder: (context) => [ itemBuilder: (context) => [
const PopupMenuItem( PopupMenuItem(
value: 'clear', value: 'clear',
child: Row( child: Row(
children: [ children: [
Icon(Icons.clear_all, size: 20), const Icon(Icons.clear_all, size: 20),
SizedBox(width: 8), const SizedBox(width: 8),
Text('清空结果'), Text('clearResults'.tr), //清空结果
], ],
), ),
), ),
const PopupMenuItem( PopupMenuItem(
value: 'settings', value: 'settings',
child: Row( child: Row(
children: [ children: [
Icon(Icons.settings, size: 20), const Icon(Icons.settings, size: 20),
SizedBox(width: 8), const SizedBox(width: 8),
Text('设置'), 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<PictureTranslationController> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'选择图片来源', '选择图片来源'.tr, //选择图片来源
style: TextStyle( style: TextStyle(
fontSize: 16.sp, fontSize: 16.sp,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -144,7 +167,7 @@ class PictureTranslationView extends GetView<PictureTranslationController> {
Expanded( Expanded(
child: _buildActionButton( child: _buildActionButton(
icon: Icons.photo_library, icon: Icons.photo_library,
label: '相册', label: 'photoAlbum'.tr,
onTap: controller.pickFromGallery, onTap: controller.pickFromGallery,
color: Colors.blue, color: Colors.blue,
), ),
@ -153,7 +176,7 @@ class PictureTranslationView extends GetView<PictureTranslationController> {
Expanded( Expanded(
child: _buildActionButton( child: _buildActionButton(
icon: Icons.camera_alt, icon: Icons.camera_alt,
label: '拍照', label: 'takePhoto'.tr,
onTap: controller.pickFromCamera, onTap: controller.pickFromCamera,
color: Colors.green, color: Colors.green,
), ),
@ -203,128 +226,29 @@ class PictureTranslationView extends GetView<PictureTranslationController> {
); );
} }
/// 构建语言选择器 Future<void> _showLanguageDialog(bool isSource) async {
Widget _buildLanguageSelector() { final isDarkMode = Get.isDarkMode; // 使用Get.isDarkMode替代Theme.of(context)
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<void> _showLangDialog(bool isSource) async {
final langs = controller.languageManager.getAllChineseNames(); final langs = controller.languageManager.getAllChineseNames();
final selected = await Get.dialog<String>( final currentLanguage = isSource
SimpleDialog( ? controller.sourceLanguageChineseName.value
title: Text(isSource ? '选择源语言' : '选择目标语言'), : controller.targetLanguageChineseName.value;
children: [
SizedBox( // 复用统一语言选择弹窗组件(保持中文名直接显示)
width: 320.w, LanguageSelectionDialog.show(
height: 420.h, isSource,
child: ListView.builder( isDarkMode,
itemCount: langs.length, languages: langs,
itemBuilder: (_, i) => ListTile( currentLanguage: currentLanguage,
title: Text(langs[i]), includeAutoDetect: true, // 图片翻译也支持自动检测
onTap: () => Get.back(result: langs[i]), 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() { Widget _buildImageSection() {
@ -577,7 +501,7 @@ class PictureTranslationView extends GetView<PictureTranslationController> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
'$title ($language)', '$title (${language.tr})',
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -588,11 +512,11 @@ class PictureTranslationView extends GetView<PictureTranslationController> {
icon: Icon(Icons.copy, size: 18.sp, color: color), icon: Icon(Icons.copy, size: 18.sp, color: color),
onPressed: () { onPressed: () {
Clipboard.setData(ClipboardData(text: content)); Clipboard.setData(ClipboardData(text: content));
Get.snackbar('成功', '已复制到剪切板'); Get.snackbar('success'.tr, 'copied'.tr); // 翻译键代表的中文文本内容
}, },
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(), constraints: const BoxConstraints(),
tooltip: '复制', tooltip: 'copy'.tr, // 翻译键代表的中文文本内容
), ),
], ],
), ),
@ -629,7 +553,7 @@ class PictureTranslationView extends GetView<PictureTranslationController> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
'最近翻译', 'translationHistory'.tr, // 翻译键代表的中文文本内容
style: TextStyle( style: TextStyle(
fontSize: 16.sp, fontSize: 16.sp,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -638,7 +562,7 @@ class PictureTranslationView extends GetView<PictureTranslationController> {
), ),
TextButton( TextButton(
onPressed: _showHistoryBottomSheet, onPressed: _showHistoryBottomSheet,
child: const Text('查看全部'), child: Text('viewAll'.tr), // 翻译键代表的中文文本内容
), ),
], ],
), ),
@ -648,7 +572,7 @@ class PictureTranslationView extends GetView<PictureTranslationController> {
padding: padding:
EdgeInsets.symmetric(horizontal: 16.w).copyWith(bottom: 16.h), EdgeInsets.symmetric(horizontal: 16.w).copyWith(bottom: 16.h),
child: Text( child: Text(
'暂无翻译记录', 'noTranslationHistory'.tr, // 翻译键代表的中文文本内容
style: TextStyle(fontSize: 14.sp, color: Colors.grey[600]), style: TextStyle(fontSize: 14.sp, color: Colors.grey[600]),
), ),
) )
@ -707,7 +631,7 @@ class PictureTranslationView extends GetView<PictureTranslationController> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
'翻译历史', 'translationHistory'.tr, // 翻译键代表的中文文本内容
style: style:
TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold), TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold),
), ),
@ -724,7 +648,7 @@ class PictureTranslationView extends GetView<PictureTranslationController> {
if (controller.historyList.isEmpty) { if (controller.historyList.isEmpty) {
return Center( return Center(
child: Text( child: Text(
'暂无翻译记录', 'noTranslationHistory'.tr, // 翻译键代表的中文文本内容
style: style:
TextStyle(fontSize: 16.sp, color: Colors.grey[600]), TextStyle(fontSize: 16.sp, color: Colors.grey[600]),
), ),
@ -924,29 +848,33 @@ class PictureTranslationView extends GetView<PictureTranslationController> {
], ],
), ),
) )
: ClipRRect( : GestureDetector(
borderRadius: BorderRadius.circular(8.r), // 点击原图预览打开查看器(与翻译后的图片预览一致)
child: Image.file( onTap: () => _openImageViewer(FileImage(file)),
file, child: ClipRRect(
fit: BoxFit.cover, borderRadius: BorderRadius.circular(8.r),
width: double.infinity, child: Image.file(
errorBuilder: (context, error, stackTrace) { file,
return Center( fit: BoxFit.cover,
child: Column( width: double.infinity,
mainAxisAlignment: MainAxisAlignment.center, errorBuilder: (context, error, stackTrace) {
children: [ return Center(
Icon(Icons.error_outline, child: Column(
size: 48.sp, color: Colors.red[400]), mainAxisAlignment: MainAxisAlignment.center,
SizedBox(height: 8.h), children: [
Text( Icon(Icons.error_outline,
'图片加载失败', size: 48.sp, color: Colors.red[400]),
style: TextStyle( SizedBox(height: 8.h),
fontSize: 14.sp, color: Colors.red[600]), Text(
), '图片加载失败',
], style: TextStyle(
), fontSize: 14.sp, color: Colors.grey[600]),
); ),
}, ],
),
);
},
),
), ),
), ),
); );

186
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/volcano_translation_service.dart';
import '../../../data/services/language_manager.dart'; import '../../../data/services/language_manager.dart';
import '../../../core/utils/logger.dart'; import '../../../core/utils/logger.dart';
import '../../translation/views/dialogs/language_selection_dialog.dart';
/// 文本翻译控制器 /// 文本翻译控制器
class TextTranslationController extends GetxController { class TextTranslationController extends GetxController {
@ -131,7 +132,7 @@ class TextTranslationController extends GetxController {
sourceLanguageChineseName.value = language; sourceLanguageChineseName.value = language;
// 同时更新语言代码;当选择“自动检测”时,将源语言代码置空,后续翻译流程会进行语种检测 // 同时更新语言代码;当选择“自动检测”时,将源语言代码置空,后续翻译流程会进行语种检测
if (language == '自动检测') { if (language == 'autoDetect') {
sourceLanguageCode.value = ''; sourceLanguageCode.value = '';
} else { } else {
final asrCode = _languageManager.getAsrCodeByChineseName(language); final asrCode = _languageManager.getAsrCodeByChineseName(language);
@ -248,7 +249,7 @@ class TextTranslationController extends GetxController {
Logger.info('调用翻译服务...'); Logger.info('调用翻译服务...');
// 若选择了“自动检测”,先进行语种检测 // 若选择了“自动检测”,先进行语种检测
if (sourceLanguageChineseName.value == '自动检测' || if (sourceLanguageChineseName.value == 'autoDetect' ||
sourceLanguageCode.value.isEmpty) { sourceLanguageCode.value.isEmpty) {
Logger.info('源语言为自动检测,开始语种检测…'); Logger.info('源语言为自动检测,开始语种检测…');
final detectedAsrCode = final detectedAsrCode =
@ -256,8 +257,8 @@ class TextTranslationController extends GetxController {
if (detectedAsrCode == null || detectedAsrCode.isEmpty) { if (detectedAsrCode == null || detectedAsrCode.isEmpty) {
Logger.error('语种检测失败'); Logger.error('语种检测失败');
Get.snackbar( Get.snackbar(
'错误', // 翻译键代表的中文文本内容 'error'.tr, // 翻译键代表的中文文本内容
'语种检测失败,请手动选择源语言或稍后重试', // 翻译键代表的中文文本内容 'languageDetectionFailed'.tr, // 翻译键代表的中文文本内容
snackPosition: SnackPosition.TOP, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.9), backgroundColor: Colors.red.withOpacity(0.9),
colorText: Colors.white, colorText: Colors.white,
@ -322,164 +323,23 @@ class TextTranslationController extends GetxController {
/// 显示语言选择对话框(统一与其它翻译模式的样式) /// 显示语言选择对话框(统一与其它翻译模式的样式)
void showLanguageSelector({required bool isSourceLanguage}) { void showLanguageSelector({required bool isSourceLanguage}) {
final isDarkMode = Theme.of(Get.context!).brightness == Brightness.dark; final isDarkMode = Theme.of(Get.context!).brightness == Brightness.dark;
// 复用统一语言选择弹窗组件
Get.dialog(Dialog( LanguageSelectionDialog.show(
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, isSourceLanguage,
shape: RoundedRectangleBorder( isDarkMode,
borderRadius: BorderRadius.circular(16.r), // 统一圆角尺寸 languages: supportedLanguages,
), currentLanguage: isSourceLanguage
alignment: ? sourceLanguageChineseName.value
isSourceLanguage ? Alignment.centerLeft : Alignment.centerRight, : targetLanguageChineseName.value,
insetPadding: EdgeInsets.symmetric(horizontal: 10.w), includeAutoDetect: true, // 文本翻译源语言保留“自动检测”
child: SizedBox( translateLanguageNames: true, // 开启多语言显示,语言中文名作为翻译键
width: 0.4.sw, onSelected: (name) {
child: Padding( if (isSourceLanguage) {
padding: EdgeInsets.all(12.w), setSourceLanguage(name);
child: Column( } else {
mainAxisSize: MainAxisSize.min, setTargetLanguage(name);
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,
),
),
),
),
],
),
),
),
));
} }
} }

88
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:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
import '../../common/widgets/language_selector_bar.dart';
import '../controllers/text_translation_controller.dart'; import '../controllers/text_translation_controller.dart';
@ -93,85 +94,16 @@ class TextTranslationView extends GetView<TextTranslationController> {
); );
} }
/// 构建语言选择器(统一与其它翻译模式的样式) /// 构建语言选择器(通用组件)
Widget _buildLanguageSelector(bool isDarkMode) { Widget _buildLanguageSelector(bool isDarkMode) {
return Row( return LanguageSelectorBar(
mainAxisAlignment: MainAxisAlignment.center, sourceText: () => controller.sourceLanguageChineseName.value,
children: [ targetText: () => controller.targetLanguageChineseName.value,
// 源语言选择(与面对面模式一致的胶囊样式) onTapSource: () => controller.showLanguageSelector(isSourceLanguage: true),
Expanded( onTapTarget: () => controller.showLanguageSelector(isSourceLanguage: false),
child: GestureDetector( onSwap: controller.swapLanguages,
onTap: () => translateLanguageNames: true, // 翻译键代表的中文文本内容
controller.showLanguageSelector(isSourceLanguage: true), fontSizeSp: 15,
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,
)),
),
),
),
],
); );
} }

91
lib/modules/translation/views/dialogs/language_selection_dialog.dart

@ -5,10 +5,45 @@ import '../../controllers/translation_controller.dart';
/// 语言选择对话框 /// 语言选择对话框
class LanguageSelectionDialog { class LanguageSelectionDialog {
/// 显示语言选择对话框 /// 显示语言选择对话框(通用,支持文本/图片/语音翻译)
static void show(bool isSource, bool isDarkMode) { /// [isSource] 是否为源语言选择
final controller = Get.find<TranslationController>(); /// [isDarkMode] 深色模式标记
/// [languages] 可选,自定义语言列表(中文名称列表);若为空则使用语音翻译模块的列表
/// [currentLanguage] 当前选中的语言中文名称
/// [includeAutoDetect] 是否在源语言首项加入“自动检测”选项(仅文本翻译需要)
/// [translateLanguageNames] 是否将语言名称作为翻译键进行翻译(语音翻译保持原逻辑)
/// [onSelected] 选中项回调,参数为中文名称
static void show(
bool isSource,
bool isDarkMode, {
List<String>? languages,
String? currentLanguage,
bool includeAutoDetect = false,
bool translateLanguageNames = true,
ValueChanged<String>? onSelected,
}) {
// 若未提供自定义列表,则在已注册时回退到语音翻译控制器的列表
final hasTranslationController = Get.isRegistered<TranslationController>();
final TranslationController? controller =
hasTranslationController ? Get.find<TranslationController>() : null;
final fallbackLanguages =
controller?.supportedLanguages.keys.toList() ?? <String>[]; // 中文名称列表
final displayLanguages = List<String>.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( Get.dialog(Dialog(
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
@ -30,8 +65,8 @@ class LanguageSelectionDialog {
padding: EdgeInsets.symmetric(horizontal: 8.w), padding: EdgeInsets.symmetric(horizontal: 8.w),
child: Text( child: Text(
isSource isSource
? 'selectSourceLanguage'.tr ? 'selectSourceLanguage'.tr // 翻译键代表的中文文本内容
: 'selectTargetLanguage'.tr, // 保留源语言/目标语言区分 : 'selectTargetLanguage'.tr, // 翻译键代表的中文文本内容
style: TextStyle( style: TextStyle(
fontSize: 18.sp, // 调整字号 fontSize: 18.sp, // 调整字号
fontWeight: FontWeight.w600, // 使用数字字重 fontWeight: FontWeight.w600, // 使用数字字重
@ -47,21 +82,37 @@ class LanguageSelectionDialog {
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: children: List.generate(
controller.supportedLanguages.keys.map((language) { hasAutoDetect
return Obx(() { ? (displayLanguages.length + 1)
final currentLanguage = isSource : displayLanguages.length,
? controller.sourceLanguage.value (index) {
: controller.targetLanguage.value; // 构造展示与实际值
final isSelected = currentLanguage == language; 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( return GestureDetector(
// 改用手势检测器实现点击 // 改用手势检测器实现点击
onTap: () { onTap: () {
if (isSource) { if (onSelected != null) {
controller.setSourceLanguage(language); onSelected(valueText);
} else { } else {
controller.setTargetLanguage(language); // 默认行为:语音翻译控制器设置语言
if (isSource) {
controller?.setSourceLanguage(valueText);
} else {
controller?.setTargetLanguage(valueText);
}
} }
Get.back(); Get.back();
}, },
@ -94,7 +145,7 @@ class LanguageSelectionDialog {
// 语言名称 // 语言名称
Expanded( Expanded(
child: Text( child: Text(
language.tr, displayText,
style: TextStyle( style: TextStyle(
fontSize: 16.sp, fontSize: 16.sp,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
@ -132,8 +183,8 @@ class LanguageSelectionDialog {
), ),
), ),
); );
}); },
}).toList(), ),
), ),
), ),
), ),
@ -145,7 +196,7 @@ class LanguageSelectionDialog {
child: TextButton( child: TextButton(
onPressed: () => Get.back(), onPressed: () => Get.back(),
child: Text( child: Text(
'cancel'.tr, 'cancel'.tr, // 翻译键代表的中文文本内容
style: TextStyle( style: TextStyle(
color: isDarkMode ? const Color(0xFF6200EE) : Colors.blue, color: isDarkMode ? const Color(0xFF6200EE) : Colors.blue,
), ),

209
lib/modules/translation/views/translation_view.dart

@ -12,6 +12,7 @@ import '../views/translation_history_view.dart';
import '../bindings/translation_history_binding.dart'; import '../bindings/translation_history_binding.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
import '../../../routes/app_routes.dart'; import '../../../routes/app_routes.dart';
import '../../common/widgets/language_selector_bar.dart';
// 导入拆分后的组件 // 导入拆分后的组件
import 'widgets/face_to_face_layout.dart'; // 面对面翻译布局 import 'widgets/face_to_face_layout.dart'; // 面对面翻译布局
@ -268,108 +269,28 @@ class TranslationView extends GetView<TranslationController> {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// 原语言选择行 // 统一通用语言选择栏组件
Row( LanguageSelectorBar(
mainAxisAlignment: MainAxisAlignment.center, sourceText: () =>
children: [ controller.sourceLanguage.value.split(' ')[0],
// 源语言选择 targetText: () =>
Expanded( controller.targetLanguage.value.split(' ')[0],
child: GestureDetector( onTapSource: () =>
onTap: () => LanguageSelectionDialog.show(true, isDarkMode),
LanguageSelectionDialog.show(true, isDarkMode), onTapTarget: () =>
child: Container( LanguageSelectionDialog.show(false, isDarkMode),
padding: EdgeInsets.symmetric( onSwap: controller.swapLanguages,
horizontal: 8.w, vertical: 4.h), translateLanguageNames: true, // 翻译键代表的中文文本内容
decoration: BoxDecoration( fontSizeSp: 15,
color: isDarkMode targetSuffix: controller.currentMode.value == 'call'
? Colors.white.withOpacity(0.1) ? Icon(
: Colors.grey[100], Icons.phone_in_talk,
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,
size: 20.sp, size: 20.sp,
), color: isDarkMode
), ? Colors.white70
), : Colors.grey[600],
)
// 目标语言选择 : null,
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],
),
],
],
)),
),
),
),
],
), ),
// 通话模式下的“对方 / 我”标签行 // 通话模式下的“对方 / 我”标签行
@ -1182,84 +1103,14 @@ class TranslationView extends GetView<TranslationController> {
), ),
], ],
), ),
child: Row( child: LanguageSelectorBar(
mainAxisAlignment: MainAxisAlignment.center, sourceText: () => controller.sourceLanguage.value.split(' ')[0],
children: [ targetText: () => controller.targetLanguage.value.split(' ')[0],
// 源语言选择 onTapSource: () => LanguageSelectionDialog.show(true, isDarkMode),
Expanded( onTapTarget: () => LanguageSelectionDialog.show(false, isDarkMode),
child: GestureDetector( onSwap: controller.swapLanguages,
onTap: () => LanguageSelectionDialog.show(true, isDarkMode), translateLanguageNames: true, // 翻译键代表的中文文本内容
child: Container( fontSizeSp: 15,
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,
),
),
),
),
),
],
), ),
); );
} }

Loading…
Cancel
Save