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", //我
"counterPart": "Counterpart", //对方
// 通用对话按钮与选项(用于统一语言选择弹窗等)
// 翻译键代表的中文文本内容:取消/确认/自动检测
"autoDetect": "Auto Detect",
// 翻译历史页
"noTranslationHistory": "No translation history",
"clearHistory": "Clear History",

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

@ -911,6 +911,10 @@ const Map<String, String> zhCN = {
"me": "我", //我
"counterPart": "对方", //对方
// 通用对话按钮与选项(用于统一语言选择弹窗等)
// 以下翻译键的中文文本内容如注释所示
"autoDetect": "自动检测", // 自动检测
// 翻译历史页 (translation_history_view.dart)
"noTranslationHistory": "暂无历史记录",
"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) {
historyList.removeWhere((item) => item.id == id);
_saveHistory();
Get.snackbar('成功', '已删除历史记录');
// Get.snackbar('成功', '已删除历史记录');
}
/// 清空历史记录
void clearHistory() {
historyList.clear();
_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: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<PictureTranslationController> {
const PictureTranslationView({super.key});
@ -30,8 +33,6 @@ class PictureTranslationView extends GetView<PictureTranslationController> {
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<PictureTranslationController> {
/// 构建应用栏
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<PictureTranslationController> {
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<String>(
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<PictureTranslationController> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'选择图片来源',
'选择图片来源'.tr, //选择图片来源
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
@ -144,7 +167,7 @@ class PictureTranslationView extends GetView<PictureTranslationController> {
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<PictureTranslationController> {
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<PictureTranslationController> {
);
}
/// 构建语言选择器
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<void> _showLangDialog(bool isSource) async {
Future<void> _showLanguageDialog(bool isSource) async {
final isDarkMode = Get.isDarkMode; // 使用Get.isDarkMode替代Theme.of(context)
final langs = controller.languageManager.getAllChineseNames();
final selected = await Get.dialog<String>(
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<PictureTranslationController> {
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<PictureTranslationController> {
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<PictureTranslationController> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'最近翻译',
'translationHistory'.tr, // 翻译键代表的中文文本内容
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
@ -638,7 +562,7 @@ class PictureTranslationView extends GetView<PictureTranslationController> {
),
TextButton(
onPressed: _showHistoryBottomSheet,
child: const Text('查看全部'),
child: Text('viewAll'.tr), // 翻译键代表的中文文本内容
),
],
),
@ -648,7 +572,7 @@ class PictureTranslationView extends GetView<PictureTranslationController> {
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<PictureTranslationController> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'翻译历史',
'translationHistory'.tr, // 翻译键代表的中文文本内容
style:
TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold),
),
@ -724,7 +648,7 @@ class PictureTranslationView extends GetView<PictureTranslationController> {
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<PictureTranslationController> {
],
),
)
: 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]),
),
],
),
);
},
),
),
),
);

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/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);
}
},
);
}
}

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: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<TextTranslationController> {
);
}
/// 构建语言选择器(统一与其它翻译模式的样式)
/// 构建语言选择器(通用组件)
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,
);
}

91
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<TranslationController>();
/// 显示语言选择对话框(通用,支持文本/图片/语音翻译)
/// [isSource] 是否为源语言选择
/// [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(
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,
),

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 '../../../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<TranslationController> {
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<TranslationController> {
),
],
),
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,
),
);
}

Loading…
Cancel
Save