Browse Source

feat(翻译): 新增图片翻译和文本翻译功能模块

添加图片翻译和文本翻译的完整功能实现,包括:
1. 新增翻译模块的路由和页面绑定
2. 实现图片翻译的控制器、模型和视图
3. 实现文本翻译的控制器和视图
4. 添加多语言支持
5. 完善翻译历史记录功能
weicu
lxm 12 months ago
parent
commit
30098df5aa
  1. 2
      lib/core/translations/language/en_us.dart
  2. 2
      lib/core/translations/language/zh_cn.dart
  3. 14
      lib/modules/picture_translation/bindings/picture_translation_binding.dart
  4. 401
      lib/modules/picture_translation/controllers/picture_translation_controller.dart
  5. 134
      lib/modules/picture_translation/models/picture_translation_models.dart
  6. 299
      lib/modules/picture_translation/views/picture_translation_history_detail_view.dart
  7. 917
      lib/modules/picture_translation/views/picture_translation_view.dart
  8. 11
      lib/modules/text_translation/bindings/text_translation_binding.dart
  9. 336
      lib/modules/text_translation/controllers/text_translation_controller.dart
  10. 395
      lib/modules/text_translation/views/text_translation_view.dart
  11. 37
      lib/modules/translation/middleware/controllers/translation_middleware_controller.dart
  12. 35
      lib/modules/translation/middleware/views/translation_middleware_view.dart
  13. 18
      lib/routes/app_pages.dart
  14. 4
      lib/routes/app_routes.dart

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

@ -977,6 +977,8 @@ const Map<String, String> enUS = {
// 翻译模块
// 翻译中间层 (translation_middleware_view.dart)
"pictureTranslation": "Picture Translation",
"textTranslation": "Text Translation",
// 翻译主页
"translationHistory": "Translation History",

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

@ -897,6 +897,8 @@ const Map<String, String> zhCN = {
// 翻译模块
// 翻译中间层 (translation_middleware_view.dart)
"pictureTranslation": "图片翻译",
"textTranslation": "文本翻译",
// 翻译主页 (translation_view.dart)
"translationHistory": "翻译历史",

14
lib/modules/picture_translation/bindings/picture_translation_binding.dart

@ -0,0 +1,14 @@
import 'package:get/get.dart';
import '../controllers/picture_translation_controller.dart';
class PictureTranslationBinding extends Bindings {
@override
void dependencies() {
if (!Get.isRegistered<PictureTranslationController>()) {
Get.lazyPut<PictureTranslationController>(() => PictureTranslationController());
}
}
}

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

@ -0,0 +1,401 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
// import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart';
import 'package:image_picker/image_picker.dart';
import 'package:image/image.dart' as img;
import 'package:path/path.dart' as path;
import 'package:permission_handler/permission_handler.dart';
import '../../../data/services/volcano_translation_service.dart';
import '../../../data/services/language_manager.dart';
import '../../../core/utils/logger.dart';
import '../models/picture_translation_models.dart';
class PictureTranslationController extends GetxController {
final ImagePicker _imagePicker = ImagePicker();
// final TextRecognizer _textRecognizer =
// TextRecognizer(script: TextRecognitionScript.chinese);
final GetStorage _storage = GetStorage();
final VolcanoTranslationService _translationService =
Get.find<VolcanoTranslationService>();
final LanguageManager languageManager = Get.find<LanguageManager>();
// UI状态
final sourceLanguageChineseName = '中文(简体)'.obs;
final targetLanguageChineseName = '英语'.obs;
final sourceText = ''.obs;
final translatedText = ''.obs;
final isProcessing = false.obs;
final pickedImageFile = Rx<File?>(null);
final currentStep = ProcessingStep(
title: '等待操作',
description: '请选择图片开始翻译',
status: ProcessingStatus.idle,
).obs;
// 历史记录
final historyList = <PictureTranslationHistory>[].obs;
final isLoadingHistory = false.obs;
// 图片处理相关
final imageQuality = 85.obs;
final maxImageSize = 1920.obs;
static const String _historyKey = 'picture_translation_history';
static const String _settingsKey = 'picture_translation_settings';
@override
void onInit() {
super.onInit();
_loadSettings();
_loadHistory();
}
@override
void onClose() {
// _textRecognizer.close();
super.onClose();
}
/// 加载设置
void _loadSettings() {
try {
final settings = _storage.read(_settingsKey) as Map<String, dynamic>?;
if (settings != null) {
sourceLanguageChineseName.value =
settings['sourceLanguage'] ?? '中文(简体)';
targetLanguageChineseName.value = settings['targetLanguage'] ?? '英语';
imageQuality.value = settings['imageQuality'] ?? 85;
maxImageSize.value = settings['maxImageSize'] ?? 1920;
}
} catch (e) {
Logger.error('PictureTranslation', '加载设置失败: $e');
}
}
/// 保存设置
void _saveSettings() {
try {
_storage.write(_settingsKey, {
'sourceLanguage': sourceLanguageChineseName.value,
'targetLanguage': targetLanguageChineseName.value,
'imageQuality': imageQuality.value,
'maxImageSize': maxImageSize.value,
});
} catch (e) {
Logger.error('PictureTranslation', '保存设置失败: $e');
}
}
/// 加载历史记录
void _loadHistory() {
try {
isLoadingHistory.value = true;
final historyData = _storage.read(_historyKey) as List<dynamic>?;
if (historyData != null) {
historyList.value = historyData
.map((item) => PictureTranslationHistory.fromJson(
item as Map<String, dynamic>))
.toList();
// 按时间倒序排列
historyList.sort((a, b) => b.timestamp.compareTo(a.timestamp));
}
} catch (e) {
Logger.error('PictureTranslation', '加载历史记录失败: $e');
} finally {
isLoadingHistory.value = false;
}
}
/// 保存历史记录
void _saveHistory() {
try {
final historyData = historyList.map((item) => item.toJson()).toList();
_storage.write(_historyKey, historyData);
} catch (e) {
Logger.error('PictureTranslation', '保存历史记录失败: $e');
}
}
/// 添加历史记录
void _addToHistory(PictureTranslationHistory history) {
historyList.insert(0, history);
// 限制历史记录数量,最多保存100条
if (historyList.length > 100) {
historyList.removeRange(100, historyList.length);
}
_saveHistory();
}
/// 删除历史记录
void deleteHistory(String id) {
historyList.removeWhere((item) => item.id == id);
_saveHistory();
Get.snackbar('成功', '已删除历史记录');
}
/// 清空历史记录
void clearHistory() {
historyList.clear();
_storage.remove(_historyKey);
Get.snackbar('成功', '已清空所有历史记录');
}
/// 检查权限
Future<bool> _checkPermissions(ImageSource source) async {
if (source == ImageSource.camera) {
final status = await Permission.camera.request();
if (!status.isGranted) {
Get.snackbar('权限需求', '需要相机权限才能拍照');
return false;
}
} else {
final status = await Permission.photos.request();
if (!status.isGranted) {
Get.snackbar('权限需求', '需要相册权限才能选择图片');
return false;
}
}
return true;
}
/// 更新处理步骤
void _updateStep(String title, String description, ProcessingStatus status,
{double progress = 0.0, String? error}) {
currentStep.value = ProcessingStep(
title: title,
description: description,
status: status,
progress: progress,
error: error,
);
}
/// 从相册选择
Future<void> pickFromGallery() async {
//if (!(await _checkPermissions(ImageSource.gallery))) return;
await _pickAndProcess(ImageSource.gallery);
}
/// 从相机拍照
Future<void> pickFromCamera() async {
//if (!(await _checkPermissions(ImageSource.camera))) return;
await _pickAndProcess(ImageSource.camera);
}
/// 选择并处理图片
Future<void> _pickAndProcess(ImageSource source) async {
try {
isProcessing.value = true;
_updateStep(
'选择图片',
'正在打开${source == ImageSource.camera ? '相机' : '相册'}...',
ProcessingStatus.picking);
final XFile? image = await _imagePicker.pickImage(
source: source,
imageQuality: imageQuality.value,
maxWidth: maxImageSize.value.toDouble(),
maxHeight: maxImageSize.value.toDouble(),
);
if (image == null) {
_updateStep('已取消', '用户取消了选择', ProcessingStatus.idle);
return;
}
File file = File(image.path);
// 图片预处理
file = await _preprocessImage(file);
pickedImageFile.value = file;
_updateStep('识别文本', '正在分析图片中的文本...', ProcessingStatus.recognizing,
progress: 0.3);
// 文本识别
// final inputImage = InputImage.fromFile(file);
// final RecognizedText recognizedText =
// await _textRecognizer.processImage(inputImage);
// sourceText.value = recognizedText.text.trim();
if (sourceText.value.isEmpty) {
_updateStep('识别完成', '未识别到任何文本', ProcessingStatus.idle);
translatedText.value = '';
return;
}
_updateStep('翻译文本', '正在翻译识别的文本...', ProcessingStatus.translating,
progress: 0.7);
// 执行翻译
await _performTranslation();
// 保存历史记录
if (sourceText.value.isNotEmpty && translatedText.value.isNotEmpty) {
_updateStep('保存记录', '正在保存翻译结果...', ProcessingStatus.saving,
progress: 0.9);
await _saveTranslationResult(file);
}
_updateStep('翻译完成', '所有步骤已完成', ProcessingStatus.idle, progress: 1.0);
} catch (e) {
Logger.error('PictureTranslation', '处理失败: $e');
_updateStep('处理失败', e.toString(), ProcessingStatus.error);
_showErrorDialog('处理失败', '图片处理或翻译失败,请重试。\n错误信息: $e');
} finally {
isProcessing.value = false;
}
}
/// 图片预处理
Future<File> _preprocessImage(File file) async {
try {
final bytes = await file.readAsBytes();
final image = img.decodeImage(bytes);
if (image == null) return file;
// 自动旋转图片
final orientedImage = img.bakeOrientation(image);
// 压缩图片大小(如果需要)
img.Image resizedImage = orientedImage;
if (orientedImage.width > maxImageSize.value ||
orientedImage.height > maxImageSize.value) {
resizedImage = img.copyResize(
orientedImage,
width: orientedImage.width > orientedImage.height
? maxImageSize.value
: null,
height: orientedImage.height > orientedImage.width
? maxImageSize.value
: null,
);
}
// 保存处理后的图片
final processedBytes =
img.encodeJpg(resizedImage, quality: imageQuality.value);
final processedFile = File('${file.path}_processed.jpg');
await processedFile.writeAsBytes(processedBytes);
return processedFile;
} catch (e) {
Logger.error('PictureTranslation', '图片预处理失败: $e');
return file; // 返回原图片
}
}
/// 执行翻译
Future<void> _performTranslation() async {
final sourceCode = languageManager
.getAsrCodeByChineseName(sourceLanguageChineseName.value) ??
'zh-CN';
final targetCode = languageManager
.getAsrCodeByChineseName(targetLanguageChineseName.value) ??
'en-US';
final result = await _translationService.translateText(
text: sourceText.value,
sourceLanguageCode: sourceCode,
targetLanguageCode: targetCode,
);
translatedText.value = result ?? '';
}
/// 保存翻译结果
Future<void> _saveTranslationResult(File imageFile) async {
try {
final sourceCode = languageManager
.getAsrCodeByChineseName(sourceLanguageChineseName.value) ??
'zh-CN';
final targetCode = languageManager
.getAsrCodeByChineseName(targetLanguageChineseName.value) ??
'en-US';
final history = PictureTranslationHistory(
id: DateTime.now().millisecondsSinceEpoch.toString(),
imagePath: imageFile.path,
sourceText: sourceText.value,
translatedText: translatedText.value,
sourceLanguageCode: sourceCode,
targetLanguageCode: targetCode,
timestamp: DateTime.now(),
);
_addToHistory(history);
} catch (e) {
Logger.error('PictureTranslation', '保存翻译结果失败: $e');
}
}
/// 显示错误对话框
void _showErrorDialog(String title, String message) {
Get.dialog(
AlertDialog(
title: Text(title),
content: Text(message),
actions: [
TextButton(
onPressed: () => Get.back(),
child: const Text('确定'),
),
],
),
);
}
/// 设置源语言
void setSourceLanguage(String language) {
if (sourceLanguageChineseName.value != language) {
sourceLanguageChineseName.value = language;
_saveSettings();
}
}
/// 设置目标语言
void setTargetLanguage(String language) {
if (targetLanguageChineseName.value != language) {
targetLanguageChineseName.value = language;
_saveSettings();
}
}
/// 交换语言
void swapLanguages() {
final temp = sourceLanguageChineseName.value;
sourceLanguageChineseName.value = targetLanguageChineseName.value;
targetLanguageChineseName.value = temp;
// 交换文本
final tempText = sourceText.value;
sourceText.value = translatedText.value;
translatedText.value = tempText;
_saveSettings();
}
/// 复制文本
void copyText(String text) {
if (text.isNotEmpty) {
// 这里可以添加复制到剪切板的逻辑
Get.snackbar('成功', '文本已复制到剪切板');
}
}
/// 重置状态
void reset() {
sourceText.value = '';
translatedText.value = '';
pickedImageFile.value = null;
_updateStep('等待操作', '请选择图片开始翻译', ProcessingStatus.idle);
}
}

134
lib/modules/picture_translation/models/picture_translation_models.dart

@ -0,0 +1,134 @@
import 'package:get/get.dart';
import '../../../data/services/language_manager.dart';
/// 图片翻译历史记录模型
class PictureTranslationHistory {
final String id;
final String imagePath;
final String sourceText;
final String translatedText;
final String sourceLanguageCode;
final String targetLanguageCode;
final DateTime timestamp;
final String? thumbnailPath; // 缩略图路径
PictureTranslationHistory({
required this.id,
required this.imagePath,
required this.sourceText,
required this.translatedText,
required this.sourceLanguageCode,
required this.targetLanguageCode,
required this.timestamp,
this.thumbnailPath,
});
/// 从JSON创建实例
factory PictureTranslationHistory.fromJson(Map<String, dynamic> json) {
return PictureTranslationHistory(
id: json['id'] ?? '',
imagePath: json['imagePath'] ?? '',
sourceText: json['sourceText'] ?? '',
translatedText: json['translatedText'] ?? '',
sourceLanguageCode: json['sourceLanguageCode'] ?? 'zh-CN',
targetLanguageCode: json['targetLanguageCode'] ?? 'en-US',
timestamp:
DateTime.parse(json['timestamp'] ?? DateTime.now().toIso8601String()),
thumbnailPath: json['thumbnailPath'],
);
}
/// 转换为JSON
Map<String, dynamic> toJson() {
return {
'id': id,
'imagePath': imagePath,
'sourceText': sourceText,
'translatedText': translatedText,
'sourceLanguageCode': sourceLanguageCode,
'targetLanguageCode': targetLanguageCode,
'timestamp': timestamp.toIso8601String(),
'thumbnailPath': thumbnailPath,
};
}
/// 获取源语言中文名称
String get sourceLanguageName {
final languageManager = Get.find<LanguageManager>();
return languageManager.getChineseNameByAsrCode(sourceLanguageCode) ??
'未知语言';
}
/// 获取目标语言中文名称
String get targetLanguageName {
final languageManager = Get.find<LanguageManager>();
return languageManager.getChineseNameByAsrCode(targetLanguageCode) ??
'未知语言';
}
/// 创建副本
PictureTranslationHistory copyWith({
String? id,
String? imagePath,
String? sourceText,
String? translatedText,
String? sourceLanguageCode,
String? targetLanguageCode,
DateTime? timestamp,
String? thumbnailPath,
}) {
return PictureTranslationHistory(
id: id ?? this.id,
imagePath: imagePath ?? this.imagePath,
sourceText: sourceText ?? this.sourceText,
translatedText: translatedText ?? this.translatedText,
sourceLanguageCode: sourceLanguageCode ?? this.sourceLanguageCode,
targetLanguageCode: targetLanguageCode ?? this.targetLanguageCode,
timestamp: timestamp ?? this.timestamp,
thumbnailPath: thumbnailPath ?? this.thumbnailPath,
);
}
}
/// 处理状态枚举
enum ProcessingStatus {
idle, // 空闲
picking, // 选择图片
recognizing, // 识别文本
translating, // 翻译文本
saving, // 保存记录
error, // 错误状态
}
/// 处理步骤信息
class ProcessingStep {
final String title;
final String description;
final ProcessingStatus status;
final double progress;
final String? error;
ProcessingStep({
required this.title,
required this.description,
required this.status,
this.progress = 0.0,
this.error,
});
ProcessingStep copyWith({
String? title,
String? description,
ProcessingStatus? status,
double? progress,
String? error,
}) {
return ProcessingStep(
title: title ?? this.title,
description: description ?? this.description,
status: status ?? this.status,
progress: progress ?? this.progress,
error: error ?? this.error,
);
}
}

299
lib/modules/picture_translation/views/picture_translation_history_detail_view.dart

@ -0,0 +1,299 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../models/picture_translation_models.dart';
class PictureTranslationHistoryDetailView extends StatelessWidget {
final PictureTranslationHistory item;
const PictureTranslationHistoryDetailView({
super.key,
required this.item,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
size: 20.w,
color: Colors.black87,
),
onPressed: () => Get.back(),
),
centerTitle: true,
title: const Text('翻译详情'),
backgroundColor: Colors.white,
foregroundColor: Colors.black87,
elevation: 0,
// actions: [
// IconButton(
// icon: const Icon(Icons.share),
// onPressed: _shareTranslation,
// tooltip: '分享',
// ),
// ],
),
body: SafeArea(
child: SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildImageSection(),
SizedBox(height: 16.h),
_buildLanguageInfo(),
SizedBox(height: 16.h),
_buildTextSection('识别结果', item.sourceText, Colors.blue),
SizedBox(height: 16.h),
_buildTextSection('翻译结果', item.translatedText, Colors.green),
SizedBox(height: 16.h),
_buildTimeInfo(),
],
),
),
),
);
}
Widget _buildImageSection() {
return Container(
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: [
Padding(
padding: EdgeInsets.all(16.w),
child: Text(
'原图片',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
),
Container(
height: 250.h,
margin:
EdgeInsets.symmetric(horizontal: 16.w).copyWith(bottom: 16.h),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.r),
border: Border.all(color: Colors.grey[300]!),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8.r),
child: Image.file(
File(item.imagePath),
fit: BoxFit.contain,
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]),
),
],
),
);
},
),
),
),
],
),
);
}
Widget _buildLanguageInfo() {
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: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'源语言',
style: TextStyle(fontSize: 12.sp, color: Colors.grey[600]),
),
SizedBox(height: 4.h),
Text(
item.sourceLanguageName,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.blue,
),
),
],
),
),
Icon(Icons.arrow_forward, color: Colors.grey[400]),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'目标语言',
style: TextStyle(fontSize: 12.sp, color: Colors.grey[600]),
),
SizedBox(height: 4.h),
Text(
item.targetLanguageName,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.green,
),
),
],
),
),
],
),
);
}
Widget _buildTextSection(String title, String content, Color color) {
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: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: color,
),
),
IconButton(
icon: Icon(Icons.copy, size: 20.sp, color: color),
onPressed: () {
Clipboard.setData(ClipboardData(text: content));
Get.snackbar('成功', '已复制到剪切板');
},
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
tooltip: '复制',
),
],
),
SizedBox(height: 12.h),
SelectableText(
content,
style: TextStyle(
fontSize: 15.sp,
color: Colors.black87,
height: 1.5,
),
),
],
),
);
}
Widget _buildTimeInfo() {
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: 8.h),
Text(
_formatDateTime(item.timestamp),
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey[600],
),
),
],
),
);
}
String _formatDateTime(DateTime dateTime) {
return '${dateTime.year}年${dateTime.month}月${dateTime.day}日 '
'${dateTime.hour.toString().padLeft(2, '0')}:'
'${dateTime.minute.toString().padLeft(2, '0')}';
}
void _shareTranslation() {
final shareText = '原文:${item.sourceText}\n\n'
'译文:${item.translatedText}\n\n'
'翻译方向:${item.sourceLanguageName} → ${item.targetLanguageName}';
// 这里可以调用分享插件
Get.snackbar('提示', '分享功能开发中');
}
}

917
lib/modules/picture_translation/views/picture_translation_view.dart

@ -0,0 +1,917 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:image_picker/image_picker.dart';
import '../controllers/picture_translation_controller.dart';
import '../models/picture_translation_models.dart';
import 'picture_translation_history_detail_view.dart';
class PictureTranslationView extends GetView<PictureTranslationController> {
const PictureTranslationView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[50],
appBar: _buildAppBar(),
body: SafeArea(
child: Obx(() {
return CustomScrollView(
slivers: [
SliverPadding(
padding: EdgeInsets.all(16.w),
sliver: SliverList(
delegate: SliverChildListDelegate([
_buildQuickActions(),
SizedBox(height: 16.h),
_buildLanguageSelector(),
SizedBox(height: 16.h),
_buildImageSection(),
SizedBox(height: 16.h),
if (controller.currentStep.value.status !=
ProcessingStatus.idle)
_buildProcessingIndicator(),
if (controller.sourceText.value.isNotEmpty ||
controller.translatedText.value.isNotEmpty) ...[
SizedBox(height: 16.h),
_buildResultsSection(),
],
SizedBox(height: 16.h),
_buildHistorySection(),
]),
),
),
],
);
}),
),
);
}
/// 构建应用栏
PreferredSizeWidget _buildAppBar() {
return AppBar(
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
size: 20.w,
color: Colors.black87,
),
onPressed: () => Get.back(),
),
centerTitle: true,
title: const Text('图片翻译'),
backgroundColor: Colors.white,
foregroundColor: Colors.black87,
elevation: 0,
actions: [
IconButton(
icon: const Icon(Icons.history),
onPressed: _showHistoryBottomSheet,
tooltip: '历史记录',
),
PopupMenuButton<String>(
onSelected: _handleMenuAction,
itemBuilder: (context) => [
const PopupMenuItem(
value: 'clear',
child: Row(
children: [
Icon(Icons.clear_all, size: 20),
SizedBox(width: 8),
Text('清空结果'),
],
),
),
const PopupMenuItem(
value: 'settings',
child: Row(
children: [
Icon(Icons.settings, size: 20),
SizedBox(width: 8),
Text('设置'),
],
),
),
],
),
],
);
}
/// 构建快速操作区域
Widget _buildQuickActions() {
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: _buildActionButton(
icon: Icons.photo_library,
label: '相册',
onTap: controller.pickFromGallery,
color: Colors.blue,
),
),
SizedBox(width: 12.w),
Expanded(
child: _buildActionButton(
icon: Icons.camera_alt,
label: '拍照',
onTap: controller.pickFromCamera,
color: Colors.green,
),
),
],
),
],
),
);
}
/// 构建操作按钮
Widget _buildActionButton({
required IconData icon,
required String label,
required VoidCallback onTap,
required Color color,
}) {
return Material(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8.r),
child: InkWell(
onTap: controller.isProcessing.value ? null : onTap,
borderRadius: BorderRadius.circular(8.r),
child: Container(
padding: EdgeInsets.symmetric(vertical: 16.h),
child: Column(
children: [
Icon(
icon,
color: color,
size: 24.sp,
),
SizedBox(height: 4.h),
Text(
label,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: color,
),
),
],
),
),
),
);
}
/// 构建语言选择器
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 {
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]),
),
),
)
],
),
);
if (selected != null) {
if (isSource) {
controller.setSourceLanguage(selected);
} else {
controller.setTargetLanguage(selected);
}
}
}
Widget _buildImageSection() {
return Container(
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: [
Padding(
padding: EdgeInsets.all(16.w),
child: Text(
'图片预览',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
),
_buildImagePreview(),
],
),
);
}
Widget _buildProcessingIndicator() {
final step = controller.currentStep.value;
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: [
Row(
children: [
_buildStatusIcon(step.status),
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
step.title,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
if (step.description.isNotEmpty) ...[
SizedBox(height: 4.h),
Text(
step.description,
style:
TextStyle(fontSize: 14.sp, color: Colors.grey[600]),
),
],
],
),
),
],
),
if (step.progress > 0 && step.status != ProcessingStatus.error) ...[
SizedBox(height: 12.h),
LinearProgressIndicator(
value: step.progress,
backgroundColor: Colors.grey[200],
valueColor:
AlwaysStoppedAnimation<Color>(_getStatusColor(step.status)),
),
],
],
),
);
}
Widget _buildStatusIcon(ProcessingStatus status) {
switch (status) {
case ProcessingStatus.picking:
case ProcessingStatus.recognizing:
case ProcessingStatus.translating:
case ProcessingStatus.saving:
return SizedBox(
width: 20.w,
height: 20.w,
child: CircularProgressIndicator(
strokeWidth: 2,
color: _getStatusColor(status),
),
);
case ProcessingStatus.error:
return Icon(Icons.error_outline, color: Colors.red, size: 20.sp);
case ProcessingStatus.idle:
return Icon(Icons.check_circle_outline,
color: Colors.green, size: 20.sp);
}
}
Color _getStatusColor(ProcessingStatus status) {
switch (status) {
case ProcessingStatus.picking:
return Colors.blue;
case ProcessingStatus.recognizing:
return Colors.orange;
case ProcessingStatus.translating:
return Colors.purple;
case ProcessingStatus.saving:
return Colors.green;
case ProcessingStatus.error:
return Colors.red;
case ProcessingStatus.idle:
return Colors.grey;
}
}
Widget _buildResultsSection() {
return Container(
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: [
Padding(
padding: EdgeInsets.all(16.w),
child: Text(
'翻译结果',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
),
if (controller.sourceText.value.isNotEmpty)
_buildTextCard(
'识别结果',
controller.sourceText.value,
controller.sourceLanguageChineseName.value,
Colors.blue,
),
if (controller.translatedText.value.isNotEmpty)
_buildTextCard(
'翻译结果',
controller.translatedText.value,
controller.targetLanguageChineseName.value,
Colors.green,
),
],
),
);
}
Widget _buildTextCard(
String title, String content, String language, Color color) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 16.w).copyWith(bottom: 16.h),
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: color.withOpacity(0.05),
borderRadius: BorderRadius.circular(8.r),
border: Border.all(color: color.withOpacity(0.2)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'$title ($language)',
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: color,
),
),
IconButton(
icon: Icon(Icons.copy, size: 18.sp, color: color),
onPressed: () {
Clipboard.setData(ClipboardData(text: content));
Get.snackbar('成功', '已复制到剪切板');
},
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
tooltip: '复制',
),
],
),
SizedBox(height: 8.h),
SelectableText(
content,
style:
TextStyle(fontSize: 15.sp, color: Colors.black87, height: 1.4),
),
],
),
);
}
Widget _buildHistorySection() {
return Container(
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: [
Padding(
padding: EdgeInsets.all(16.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'最近翻译',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
TextButton(
onPressed: _showHistoryBottomSheet,
child: const Text('查看全部'),
),
],
),
),
if (controller.historyList.isEmpty)
Padding(
padding:
EdgeInsets.symmetric(horizontal: 16.w).copyWith(bottom: 16.h),
child: Text(
'暂无翻译记录',
style: TextStyle(fontSize: 14.sp, color: Colors.grey[600]),
),
)
else
...controller.historyList
.take(3)
.map((item) => _buildHistoryItem(item))
.toList(),
],
),
);
}
Widget _buildHistoryItem(PictureTranslationHistory item) {
return ListTile(
leading: ClipRRect(
borderRadius: BorderRadius.circular(4.r),
child: Container(
width: 40.w,
height: 40.w,
color: Colors.grey[200],
child: Icon(Icons.image, color: Colors.grey[400]),
),
),
title: Text(
item.sourceText,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 14.sp),
),
subtitle: Text(
'${item.sourceLanguageName} → ${item.targetLanguageName}',
style: TextStyle(fontSize: 12.sp, color: Colors.grey[600]),
),
trailing: Text(
_formatTime(item.timestamp),
style: TextStyle(fontSize: 12.sp, color: Colors.grey[500]),
),
onTap: () => _showHistoryDetail(item),
);
}
void _showHistoryBottomSheet() {
Get.bottomSheet(
Container(
height: 600.h,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20.r)),
),
child: Column(
children: [
Container(
padding: EdgeInsets.all(16.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'翻译历史',
style:
TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold),
),
IconButton(
onPressed: () => Get.back(),
icon: const Icon(Icons.close),
),
],
),
),
const Divider(height: 1),
Expanded(
child: Obx(() {
if (controller.historyList.isEmpty) {
return Center(
child: Text(
'暂无翻译记录',
style:
TextStyle(fontSize: 16.sp, color: Colors.grey[600]),
),
);
}
return ListView.separated(
itemCount: controller.historyList.length,
separatorBuilder: (context, index) =>
const Divider(height: 1),
itemBuilder: (context, index) {
final item = controller.historyList[index];
return _buildHistoryItem(item);
},
);
}),
),
],
),
),
isScrollControlled: true,
);
}
void _handleMenuAction(String action) {
switch (action) {
case 'clear':
controller.reset();
break;
case 'settings':
_showSettingsDialog();
break;
}
}
void _showHistoryDetail(PictureTranslationHistory item) {
Get.to(() => PictureTranslationHistoryDetailView(item: item));
}
String _formatTime(DateTime timestamp) {
final now = DateTime.now();
final difference = now.difference(timestamp);
if (difference.inDays > 0) {
return '${difference.inDays}天前';
} else if (difference.inHours > 0) {
return '${difference.inHours}小时前';
} else if (difference.inMinutes > 0) {
return '${difference.inMinutes}分钟前';
} else {
return '刚刚';
}
}
void _showSettingsDialog() {
Get.dialog(
AlertDialog(
title: const Text('设置'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.photo_size_select_actual),
title: const Text('图片质量'),
subtitle:
Obx(() => Text('当前: ${controller.imageQuality.value}%')),
onTap: () {
Get.back();
_showImageQualityDialog();
},
),
ListTile(
leading: const Icon(Icons.photo_size_select_large),
title: const Text('最大尺寸'),
subtitle:
Obx(() => Text('当前: ${controller.maxImageSize.value}px')),
onTap: () {
Get.back();
_showImageSizeDialog();
},
),
],
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: const Text('关闭'),
),
],
),
);
}
void _showImageQualityDialog() {
Get.dialog(
AlertDialog(
title: const Text('设置图片质量'),
content: Obx(() => Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('当前质量: ${controller.imageQuality.value}%'),
Slider(
value: controller.imageQuality.value.toDouble(),
min: 50,
max: 100,
divisions: 10,
onChanged: (value) {
controller.imageQuality.value = value.toInt();
},
),
],
)),
actions: [
TextButton(
onPressed: () => Get.back(),
child: const Text('确定'),
),
],
),
);
}
void _showImageSizeDialog() {
final sizes = [1280, 1920, 2560, 3840];
Get.dialog(
AlertDialog(
title: const Text('设置最大尺寸'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: sizes
.map((size) => RadioListTile<int>(
title: Text('${size}px'),
value: size,
groupValue: controller.maxImageSize.value,
onChanged: (value) {
if (value != null) {
controller.maxImageSize.value = value;
Get.back();
}
},
))
.toList(),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: const Text('取消'),
),
],
),
);
}
void _showClearHistoryDialog() {
Get.dialog(
AlertDialog(
title: const Text('清空历史'),
content: const Text('确定要清空所有翻译历史吗?此操作不可撤销。'),
actions: [
TextButton(
onPressed: () => Get.back(),
child: const Text('取消'),
),
TextButton(
onPressed: () {
controller.clearHistory();
Get.back();
},
child: Text('确定', style: TextStyle(color: Colors.red)),
),
],
),
);
}
Widget _buildImagePreview() {
final file = controller.pickedImageFile.value;
return Container(
height: 200.h,
margin: EdgeInsets.symmetric(horizontal: 16.w).copyWith(bottom: 16.h),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8.r),
border: Border.all(color: Colors.grey[300]!),
),
child: file == null
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.image_outlined,
size: 48.sp, color: Colors.grey[400]),
SizedBox(height: 8.h),
Text(
'请选择图片',
style: TextStyle(fontSize: 14.sp, color: Colors.grey[600]),
),
],
),
)
: 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]),
),
],
),
);
},
),
),
);
}
Widget _buildSourceTextBox() {
return _buildTextBox('识别结果', controller.sourceText.value);
}
Widget _buildTranslatedTextBox() {
return _buildTextBox('翻译结果', controller.translatedText.value);
}
Widget _buildTextBox(String title, String content) {
return Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.r),
border: Border.all(color: Colors.grey.shade300),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
SizedBox(height: 8.h),
Text(content.isEmpty ? '—' : content),
],
),
);
}
}

11
lib/modules/text_translation/bindings/text_translation_binding.dart

@ -0,0 +1,11 @@
import 'package:get/get.dart';
import '../controllers/text_translation_controller.dart';
/// 文本翻译页面依赖绑定
class TextTranslationBinding extends Bindings {
@override
void dependencies() {
// 注册文本翻译控制器
Get.lazyPut<TextTranslationController>(() => TextTranslationController());
}
}

336
lib/modules/text_translation/controllers/text_translation_controller.dart

@ -0,0 +1,336 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import '../../../data/services/volcano_translation_service.dart';
import '../../../data/services/language_manager.dart';
import '../../../core/utils/logger.dart';
/// 文本翻译控制器
class TextTranslationController extends GetxController {
// 依赖服务
final VolcanoTranslationService _translationService = Get.find<VolcanoTranslationService>();
final LanguageManager _languageManager = Get.find<LanguageManager>();
final GetStorage _storage = GetStorage();
// 文本输入控制器
final TextEditingController textController = TextEditingController();
// 响应式状态
final sourceLanguageChineseName = '中文(简体)'.obs;
final targetLanguageChineseName = '英语'.obs;
final sourceLanguageCode = 'zh-CN'.obs;
final targetLanguageCode = 'en-US'.obs;
final translatedText = ''.obs;
final isTranslating = false.obs;
final inputText = ''.obs;
// 支持的语言列表
List<String> get supportedLanguages => _languageManager.getAllChineseNames();
@override
void onInit() {
super.onInit();
// 初始化翻译服务
_initializeTranslationService();
_loadSavedLanguages();
// 监听文本输入变化
textController.addListener(() {
inputText.value = textController.text;
});
}
/// 初始化翻译服务
Future<void> _initializeTranslationService() async {
try {
final initialized = await _translationService.initialize();
if (!initialized) {
Logger.error('翻译服务初始化失败');
Get.snackbar(
'error'.tr,
'翻译服务初始化失败,请检查网络连接',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.9),
colorText: Colors.white,
);
} else {
Logger.info('翻译服务初始化成功');
}
} catch (e) {
Logger.error('翻译服务初始化异常: $e');
}
}
@override
void onClose() {
textController.dispose();
super.onClose();
}
/// 加载保存的语言设置
void _loadSavedLanguages() {
try {
final savedSourceLanguage = _storage.read('text_translation_source_language');
final savedTargetLanguage = _storage.read('text_translation_target_language');
if (savedSourceLanguage != null && supportedLanguages.contains(savedSourceLanguage)) {
sourceLanguageChineseName.value = savedSourceLanguage;
// 同时更新语言代码
final asrCode = _languageManager.getAsrCodeByChineseName(savedSourceLanguage);
if (asrCode != null) {
sourceLanguageCode.value = asrCode;
}
}
if (savedTargetLanguage != null && supportedLanguages.contains(savedTargetLanguage)) {
targetLanguageChineseName.value = savedTargetLanguage;
// 同时更新语言代码
final asrCode = _languageManager.getAsrCodeByChineseName(savedTargetLanguage);
if (asrCode != null) {
targetLanguageCode.value = asrCode;
}
}
} catch (e) {
Logger.error('加载语言设置失败: $e');
}
}
/// 保存语言设置
void _saveLanguageSettings() {
try {
_storage.write('text_translation_source_language', sourceLanguageChineseName.value);
_storage.write('text_translation_target_language', targetLanguageChineseName.value);
} catch (e) {
Logger.error('保存语言设置失败: $e');
}
}
/// 设置源语言
void setSourceLanguage(String language) {
if (language != sourceLanguageChineseName.value) {
sourceLanguageChineseName.value = language;
// 同时更新语言代码
final asrCode = _languageManager.getAsrCodeByChineseName(language);
if (asrCode != null) {
sourceLanguageCode.value = asrCode;
}
_saveLanguageSettings();
// 如果已有翻译结果,清空结果
if (translatedText.value.isNotEmpty) {
translatedText.value = '';
}
}
}
/// 设置目标语言
void setTargetLanguage(String language) {
if (language != targetLanguageChineseName.value) {
targetLanguageChineseName.value = language;
// 同时更新语言代码
final asrCode = _languageManager.getAsrCodeByChineseName(language);
if (asrCode != null) {
targetLanguageCode.value = asrCode;
}
_saveLanguageSettings();
// 如果已有翻译结果,清空结果
if (translatedText.value.isNotEmpty) {
translatedText.value = '';
}
}
}
/// 交换源语言和目标语言
void swapLanguages() {
final tempSource = sourceLanguageChineseName.value;
final tempTarget = targetLanguageChineseName.value;
final tempSourceCode = sourceLanguageCode.value;
final tempTargetCode = targetLanguageCode.value;
final tempInputText = textController.text;
final tempTranslatedText = translatedText.value;
sourceLanguageChineseName.value = tempTarget;
targetLanguageChineseName.value = tempSource;
sourceLanguageCode.value = tempTargetCode;
targetLanguageCode.value = tempSourceCode;
// 交换输入文本和翻译结果
textController.text = tempTranslatedText;
translatedText.value = tempInputText;
_saveLanguageSettings();
}
/// 执行翻译
Future<void> translateText() async {
final text = textController.text.trim();
// 验证输入
if (text.isEmpty) {
Get.snackbar(
'error'.tr,
'pleaseInputText'.tr,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.orange.withOpacity(0.9),
colorText: Colors.white,
);
return;
}
if (text.length > 1000) {
Get.snackbar(
'error'.tr,
'textTooLong'.tr,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.orange.withOpacity(0.9),
colorText: Colors.white,
);
return;
}
// 检查源语言和目标语言是否相同
if (sourceLanguageChineseName.value == targetLanguageChineseName.value) {
translatedText.value = text;
return;
}
isTranslating.value = true;
translatedText.value = '';
try {
Logger.info('开始翻译: $text');
Logger.info('源语言: ${sourceLanguageChineseName.value} -> ${sourceLanguageCode.value}');
Logger.info('目标语言: ${targetLanguageChineseName.value} -> ${targetLanguageCode.value}');
// 确保翻译服务已初始化
Logger.info('确保翻译服务已初始化...');
final initialized = await _translationService.initialize();
Logger.info('翻译服务初始化结果: $initialized');
if (!initialized) {
throw Exception('翻译服务初始化失败');
}
Logger.info('调用翻译服务...');
// 调用翻译服务,直接使用维护的语言代码
final result = await _translationService.translateText(
text: text,
sourceLanguageCode: sourceLanguageCode.value,
targetLanguageCode: targetLanguageCode.value,
);
Logger.info('翻译服务返回结果: $result');
if (result != null && result.isNotEmpty) {
translatedText.value = result;
Logger.info('翻译成功: $result');
} else {
Logger.error('翻译服务返回空结果');
throw Exception('翻译服务返回空结果');
}
} catch (e) {
Logger.error('翻译失败: $e');
Get.snackbar(
'error'.tr,
'translationFailed'.tr,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.9),
colorText: Colors.white,
);
} finally {
isTranslating.value = false;
}
}
/// 复制翻译结果
void copyResult() {
if (translatedText.value.isNotEmpty) {
Clipboard.setData(ClipboardData(text: translatedText.value));
Get.snackbar(
'success'.tr,
'copied'.tr,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green.withOpacity(0.9),
colorText: Colors.white,
duration: const Duration(seconds: 2),
);
}
}
/// 清空输入
void clearInput() {
textController.clear();
translatedText.value = '';
}
/// 显示语言选择对话框
void showLanguageSelector({required bool isSourceLanguage}) {
Get.bottomSheet(
Container(
height: Get.height * 0.6,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
isSourceLanguage ? 'sourceLanguage'.tr : 'targetLanguage'.tr,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
IconButton(
onPressed: () => Get.back(),
icon: const Icon(Icons.close),
),
],
),
),
const Divider(height: 1),
Expanded(
child: ListView.builder(
itemCount: supportedLanguages.length,
itemBuilder: (context, index) {
final language = supportedLanguages[index];
final isSelected = isSourceLanguage
? language == sourceLanguageChineseName.value
: language == targetLanguageChineseName.value;
return ListTile(
title: Text(language),
trailing: isSelected ? const Icon(Icons.check, color: Colors.blue) : null,
onTap: () {
if (isSourceLanguage) {
setSourceLanguage(language);
} else {
setTargetLanguage(language);
}
Get.back();
},
);
},
),
),
],
),
),
isScrollControlled: true,
);
}
}

395
lib/modules/text_translation/views/text_translation_view.dart

@ -0,0 +1,395 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../controllers/text_translation_controller.dart';
/// 文本翻译页面
class TextTranslationView extends GetView<TextTranslationController> {
const TextTranslationView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
appBar: _buildAppBar(isDarkMode),
body: SafeArea(
child: SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 语言选择区域
_buildLanguageSelector(isDarkMode),
SizedBox(height: 20.h),
// 输入区域
_buildInputSection(isDarkMode),
SizedBox(height: 20.h),
// 翻译按钮
_buildTranslateButton(isDarkMode),
SizedBox(height: 20.h),
// 翻译结果区域
_buildResultSection(isDarkMode),
],
),
),
),
);
}
/// 构建应用栏
PreferredSizeWidget _buildAppBar(bool isDarkMode) {
return AppBar(
backgroundColor: isDarkMode ? Colors.black : Colors.white,
elevation: 0,
leading: IconButton(
icon: Icon(
Icons.arrow_back,
color: isDarkMode ? Colors.white : Colors.black,
),
onPressed: () => Get.back(),
),
title: Text(
'textTranslation'.tr,
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black,
fontSize: 18.sp,
fontWeight: FontWeight.w600,
),
),
actions: [
IconButton(
icon: Icon(
Icons.clear_all,
color: isDarkMode ? Colors.white : Colors.black,
),
onPressed: controller.clearInput,
tooltip: 'clearInput'.tr,
),
],
);
}
/// 构建语言选择器
Widget _buildLanguageSelector(bool isDarkMode) {
return Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.white,
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
// 源语言选择
Obx(() => _buildLanguageButton(
label: 'sourceLanguage'.tr,
selectedLanguage: controller.sourceLanguageChineseName.value,
onTap: () => controller.showLanguageSelector(isSourceLanguage: true),
isDarkMode: isDarkMode,
)),
SizedBox(height: 12.h),
// 交换语言按钮
Center(
child: GestureDetector(
onTap: controller.swapLanguages,
child: Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.swap_vert,
color: Colors.blue,
size: 20.sp,
),
),
),
),
SizedBox(height: 12.h),
// 目标语言选择
Obx(() => _buildLanguageButton(
label: 'targetLanguage'.tr,
selectedLanguage: controller.targetLanguageChineseName.value,
onTap: () => controller.showLanguageSelector(isSourceLanguage: false),
isDarkMode: isDarkMode,
)),
],
),
);
}
/// 构建语言选择按钮
Widget _buildLanguageButton({
required String label,
required String selectedLanguage,
required VoidCallback onTap,
required bool isDarkMode,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.grey[100],
borderRadius: BorderRadius.circular(8.r),
border: Border.all(
color: isDarkMode ? Colors.grey[700]! : Colors.grey[300]!,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
),
SizedBox(height: 4.h),
Text(
selectedLanguage,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.white : Colors.black,
),
),
],
),
Icon(
Icons.keyboard_arrow_down,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
],
),
),
);
}
/// 构建输入区域
Widget _buildInputSection(bool isDarkMode) {
return Container(
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : 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: [
Padding(
padding: EdgeInsets.fromLTRB(16.w, 16.h, 16.w, 8.h),
child: Text(
'inputTextToTranslate'.tr,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.grey[300] : Colors.grey[700],
),
),
),
Container(
margin: EdgeInsets.symmetric(horizontal: 16.w),
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.grey[50],
borderRadius: BorderRadius.circular(8.r),
border: Border.all(
color: isDarkMode ? Colors.grey[700]! : Colors.grey[300]!,
),
),
child: TextField(
controller: controller.textController,
maxLines: 6,
maxLength: 1000,
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.white : Colors.black,
),
decoration: InputDecoration(
hintText: 'inputTextToTranslate'.tr,
hintStyle: TextStyle(
color: isDarkMode ? Colors.grey[500] : Colors.grey[400],
),
border: InputBorder.none,
counterStyle: TextStyle(
color: isDarkMode ? Colors.grey[500] : Colors.grey[400],
),
),
),
),
SizedBox(height: 16.h),
],
),
);
}
/// 构建翻译按钮
Widget _buildTranslateButton(bool isDarkMode) {
return Obx(() => SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: controller.isTranslating.value ? null : controller.translateText,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
elevation: 0,
disabledBackgroundColor: Colors.grey,
),
child: controller.isTranslating.value
? Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 20.w,
height: 20.w,
child: const CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
),
SizedBox(width: 12.w),
Text(
'translating'.tr,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
),
),
],
)
: Text(
'translate'.tr,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
),
),
),
));
}
/// 构建翻译结果区域
Widget _buildResultSection(bool isDarkMode) {
return Obx(() {
if (controller.translatedText.value.isEmpty) {
return const SizedBox.shrink();
}
return Container(
width: double.infinity,
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : 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: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'translationResult'.tr,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.grey[300] : Colors.grey[700],
),
),
GestureDetector(
onTap: controller.copyResult,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(6.r),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.copy,
size: 16.sp,
color: Colors.blue,
),
SizedBox(width: 4.w),
Text(
'copyResult'.tr,
style: TextStyle(
fontSize: 12.sp,
color: Colors.blue,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
],
),
SizedBox(height: 12.h),
Container(
width: double.infinity,
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.grey[50],
borderRadius: BorderRadius.circular(8.r),
border: Border.all(
color: isDarkMode ? Colors.grey[700]! : Colors.grey[300]!,
),
),
child: Text(
controller.translatedText.value,
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.white : Colors.black,
height: 1.5,
),
),
),
],
),
);
});
}
}

37
lib/modules/translation/middleware/controllers/translation_middleware_controller.dart

@ -9,36 +9,50 @@ class TranslationMiddlewareController extends GetxController {
final List<TranslationOption> translationOptions = [
TranslationOption(
translationId: 'simultaneous',
title: 'simultaneousTranslation', // 实时翻译
description: 'realtimeTranslationDescription', // 实时语音翻译,支持多种语言
title: 'simultaneousTranslation'.tr, // 实时翻译
// description: 'realtimeTranslationDescription', // 实时语音翻译,支持多种语言
icon: Icons.record_voice_over,
iconColor: 0xFF2196F3, // 蓝色
targetRoute: Routes.translation,
),
TranslationOption(
translationId: 'call',
title: 'callTranslation', // 电话翻译
description: 'simultaneousTranslationInfo', // 双向对话翻译,面对面交流
title: 'callTranslation'.tr, // 通话翻译
// description: 'simultaneousTranslationInfo', // 双向对话翻译,面对面交流
icon: Icons.call,
iconColor: 0xFF4CAF50, // 绿色
targetRoute: Routes.translation,
),
TranslationOption(
translationId: 'faceToFace',
title: 'faceToFaceTranslation', // 面对面翻译
description: 'faceToFaceTranslationDescription', // 面对面交流,支持多种语言
title: 'faceToFaceTranslation'.tr, // 面对面翻译
// description: 'faceToFaceTranslationDescription', // 面对面交流,支持多种语言
icon: Icons.people,
iconColor: 0xFFFF9800, // 橙色
targetRoute: Routes.translation,
),
TranslationOption(
translationId: 'audioVideo',
title: 'audioVideoTranslation', // 多媒体翻译
description: 'audioVideoTranslationDescription', // 支持音频和视频翻译
title: 'audioVideoTranslation'.tr, // 多媒体翻译
//description: 'audioVideoTranslationDescription', // 支持音频和视频翻译
icon: Icons.videocam,
iconColor: 0xFF9C27B0, // 紫色
targetRoute: Routes.translation,
),
TranslationOption(
translationId: 'picture_translator',
title: 'pictureTranslation'.tr, // 图片翻译
icon: Icons.image,
iconColor: 0xFF009688, // 青色
targetRoute: Routes.pictureTranslation,
),
TranslationOption(
translationId: 'text_translator',
title: 'textTranslation'.tr, // 文本翻译
icon: Icons.text_fields,
iconColor: 0xFF3F51B5, // 棕色
targetRoute: Routes.textTranslation,
),
];
/// 导航到指定的翻译功能
@ -54,7 +68,8 @@ class TranslationMiddlewareController extends GetxController {
/// 获取翻译选项的本地化描述
String getLocalizedDescription(TranslationOption option) {
return option.description.tr;
// return option.description.tr;
return '';
}
}
@ -62,7 +77,7 @@ class TranslationMiddlewareController extends GetxController {
class TranslationOption {
final String translationId;
final String title;
final String description;
// final String description;
final IconData icon;
final int iconColor;
final String targetRoute;
@ -70,7 +85,7 @@ class TranslationOption {
TranslationOption({
required this.translationId,
required this.title,
required this.description,
// required this.description,
required this.icon,
required this.iconColor,
required this.targetRoute,

35
lib/modules/translation/middleware/views/translation_middleware_view.dart

@ -38,8 +38,8 @@ class TranslationMiddlewareView
title: Text(
'interpretation'.tr, // 翻译
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.bold,
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
@ -125,7 +125,7 @@ class TranslationMiddlewareView
// 标题
Text(
controller.getLocalizedTitle(option),
option.title,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.bold,
@ -136,39 +136,10 @@ class TranslationMiddlewareView
overflow: TextOverflow.ellipsis,
),
//SizedBox(height: 8.h),
// 描述
// Text(
// controller.getLocalizedDescription(option),
// style: TextStyle(
// fontSize: 12.sp,
// color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
// height: 1.3,
// ),
// textAlign: TextAlign.center,
// maxLines: 3,
// overflow: TextOverflow.ellipsis,
// ),
],
),
),
),
);
}
/// 根据图标名称获取图标数据
IconData _getIconData(String iconName) {
switch (iconName) {
case 'translate':
return Icons.translate;
case 'people':
return Icons.people;
case 'history':
return Icons.history;
case 'bar_chart':
return Icons.bar_chart;
default:
return Icons.help_outline;
}
}
}

18
lib/routes/app_pages.dart

@ -67,6 +67,10 @@ import '../modules/payment/bindings/payment_binding.dart';
import '../modules/payment/views/payment_view.dart';
import '../modules/translation/middleware/views/translation_middleware_view.dart';
import '../modules/translation/middleware/bindings/translation_middleware_binding.dart';
import '../modules/text_translation/bindings/text_translation_binding.dart';
import '../modules/text_translation/views/text_translation_view.dart';
import '../modules/picture_translation/views/picture_translation_view.dart';
import '../modules/picture_translation/bindings/picture_translation_binding.dart';
abstract class AppPages {
static final pages = [
@ -164,6 +168,20 @@ abstract class AppPages {
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage(
name: Routes.pictureTranslation,
page: () => const PictureTranslationView(),
binding: PictureTranslationBinding(),
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage(
name: Routes.textTranslation,
page: () => const TextTranslationView(),
binding: TextTranslationBinding(),
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage(
name: Routes.translationHistory,
page: () => const TranslationHistoryView(),

4
lib/routes/app_routes.dart

@ -27,7 +27,9 @@ abstract class Routes {
static const meetingRecord = '/meeting/record';
// 语言功能
static const translationMiddleware = '/translationMiddleware'; // 翻译中间层(统一入口)
static const translation = '/translation'; // 实时翻译
static const translation = '/translation'; // 翻译
static const pictureTranslation = '/picture_translation'; // 图片翻译
static const textTranslation = '/text_translation'; // 文本翻译
static const translationHistory = '/translation/history'; // 翻译历史
static const translationStats = '/translation/stats'; // 翻译统计
static const speechDemo = '/speech_demo'; // 语音演示

Loading…
Cancel
Save