From 8b89ca9d54a100e6c79fba8241cb9f20b8e7c5f7 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Sat, 20 Sep 2025 15:48:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(translation):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E7=BF=BB=E8=AF=91=E6=9D=A1=E7=9B=AE=E9=95=BF=E6=8C=89=E8=8F=9C?= =?UTF-8?q?=E5=8D=95=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为翻译条目添加长按菜单,支持复制、删除单条和全部记录 - 实现触觉反馈增强用户体验 - 添加确认对话框防止误操作 --- .../translation/views/translation_view.dart | 497 +++++++++++++++--- 1 file changed, 412 insertions(+), 85 deletions(-) diff --git a/lib/modules/translation/views/translation_view.dart b/lib/modules/translation/views/translation_view.dart index a5b21aa2a..6b9ed4eab 100644 --- a/lib/modules/translation/views/translation_view.dart +++ b/lib/modules/translation/views/translation_view.dart @@ -2,8 +2,10 @@ import 'dart:ui'; 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:haptic_feedback/haptic_feedback.dart'; import '../controllers/translation_controller.dart'; import '../models/translation_models.dart'; import '../views/translation_history_view.dart'; @@ -414,103 +416,113 @@ class TranslationView extends GetView { Widget _buildTranslationItemWidget( BuildContext context, TranslationItem item) { - return Container( - margin: const EdgeInsets.only(bottom: 8), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: item.isIntermediate - ? Colors.grey.withOpacity(0.1) // 中间结果使用浅灰色底色 - : (item.sourceLanguageCode == controller.sourceLanguageCode.value - ? Theme.of(context) - .colorScheme - .primary - .withOpacity(0.08) // 源语言匹配时使用主题色底色 - : Theme.of(context) - .colorScheme - .secondary - .withOpacity(0.08)), // 否则使用次要色底色 - borderRadius: BorderRadius.circular(12), - border: Border.all( + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return GestureDetector( + onLongPressStart: (details) => _onLongPressTranslationItem( + context, + isDarkMode, + details, + item, + ), + child: Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( color: item.isIntermediate - ? Colors.grey.withOpacity(0.2) - : Colors.transparent, - width: 1, + ? Colors.grey.withOpacity(0.1) // 中间结果使用浅灰色底色 + : (item.sourceLanguageCode == controller.sourceLanguageCode.value + ? Theme.of(context) + .colorScheme + .primary + .withOpacity(0.08) // 源语言匹配时使用主题色底色 + : Theme.of(context) + .colorScheme + .secondary + .withOpacity(0.08)), // 否则使用次要色底色 + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: item.isIntermediate + ? Colors.grey.withOpacity(0.2) + : Colors.transparent, + width: 1, + ), ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 源文本和删除按钮行 - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 源文本 - Expanded( - child: Text( - item.sourceText, - style: TextStyle( - fontSize: 16, - color: Theme.of(context).textTheme.bodyLarge?.color, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 源文本和删除按钮行 + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 源文本 + Expanded( + child: Text( + item.sourceText, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).textTheme.bodyLarge?.color, + ), ), ), - ), - // 删除按钮 - 只对完成态记录显示,与源文本平齐 - if (!item.isIntermediate) - GestureDetector( - onTap: () => _showDeleteItemConfirmDialog(item), - child: Container( - padding: EdgeInsets.all(4.w), - child: Icon( - Icons.delete_outline, - size: 18.sp, - color: Colors.grey[500], + // 删除按钮 - 只对完成态记录显示,与源文本平齐 + if (!item.isIntermediate) + GestureDetector( + onTap: () => _showDeleteItemConfirmDialog(item), + child: Container( + padding: EdgeInsets.all(4.w), + child: Icon( + Icons.delete_outline, + size: 18.sp, + color: Colors.grey[500], + ), ), ), - ), - ], - ), - if (item.translatedText.isNotEmpty) ...[ - const SizedBox(height: 8), - // 翻译文本 - Text( - item.translatedText, - style: TextStyle( - fontSize: 16, - color: Theme.of(context).textTheme.bodyMedium?.color, - fontWeight: FontWeight.w500, - ), + ], ), - ], - // 语言信息和时间戳 - const SizedBox(height: 4), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - '${item.sourceLanguageName.tr} → ${item.targetLanguageName.tr}', - style: TextStyle( - fontSize: 12, - color: Theme.of(context).textTheme.bodySmall?.color, - ), + if (item.translatedText.isNotEmpty) ...[ + const SizedBox(height: 8), + // 翻译文本 + Text( + item.translatedText, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).textTheme.bodyMedium?.color, + fontWeight: FontWeight.w500, ), ), - // 播放按钮 - if (!item.isIntermediate) - GestureDetector( - onTap: () => controller.playTranslationItem(item), - child: Container( - padding: EdgeInsets.all(4.w), - child: Icon( - Icons.volume_up, - size: 20.sp, - color: Theme.of(context).colorScheme.primary, + ], + // 语言信息和时间戳 + const SizedBox(height: 4), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + '${item.sourceLanguageName.tr} → ${item.targetLanguageName.tr}', + style: TextStyle( + fontSize: 12, + color: Theme.of(context).textTheme.bodySmall?.color, ), ), ), - ], - ), - ], + // 播放按钮 + if (!item.isIntermediate) + GestureDetector( + onTap: () => controller.playTranslationItem(item), + child: Container( + padding: EdgeInsets.all(4.w), + child: Icon( + Icons.volume_up, + size: 20.sp, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ], + ), + ], + ), ), ); } @@ -874,6 +886,168 @@ class TranslationView extends GetView { }); } + /// 处理翻译条目长按事件 + void _onLongPressTranslationItem( + BuildContext context, + bool isDarkMode, + LongPressStartDetails details, + TranslationItem item, + ) { + // 只对完成态记录显示长按菜单 + if (item.isIntermediate) return; + + // 触发触觉反馈 + Haptics.vibrate(HapticsType.success); + + // 计算弹出菜单位置 + RenderBox? renderBox = + Overlay.of(context).context.findRenderObject() as RenderBox; + final RelativeRect position = RelativeRect.fromRect( + Rect.fromLTRB( + details.globalPosition.dx, + details.globalPosition.dy, + details.globalPosition.dx + 110, // 菜单显示位置X轴坐标 + details.globalPosition.dy - 40, // 菜单显示位置Y轴坐标 + ), + Offset.zero & renderBox.size, + ); + + // 显示弹出菜单 + showMenu( + context: context, + position: position, + color: isDarkMode ? AppColors.cardBackground : Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.r), + ), + items: _buildTranslationMenuItems(context, isDarkMode, item), + ); + } + + /// 构建翻译条目弹出菜单项 + List _buildTranslationMenuItems( + BuildContext context, + bool isDarkMode, + TranslationItem item, + ) { + return [ + // 复制选项 + _buildPopupMenuItem( + Icon( + Icons.copy, + size: 20.w, + color: isDarkMode ? Colors.white : Colors.black87, + ), + 'copy'.tr, // 复制 + isDarkMode ? Colors.white : Colors.black87, + () => _copyTranslationItem(item), + ), + + // 分隔线 + PopupMenuItem( + height: 1, + enabled: false, + child: Divider( + height: 1, + thickness: 0.8, + indent: 0, + endIndent: 0, + color: isDarkMode ? Colors.grey[700] : Colors.grey[300], + ), + ), + + // 删除该条记录选项 + _buildPopupMenuItem( + Icon( + Icons.delete_outline, + size: 20.w, + color: isDarkMode ? Colors.orange[300] : Colors.orange, + ), + '删除该条记录', + isDarkMode ? Colors.orange[300]! : Colors.orange, + () => _showDeleteItemConfirmDialog(item), + ), + + // 分隔线 + PopupMenuItem( + height: 1, + enabled: false, + child: Divider( + height: 1, + thickness: 0.8, + indent: 0, + endIndent: 0, + color: isDarkMode ? Colors.grey[700] : Colors.grey[300], + ), + ), + + // 删除所有记录选项 + _buildPopupMenuItem( + Icon( + Icons.delete_sweep, + size: 20.w, + color: Colors.red, + ), + '删除所有翻译历史', + Colors.red, + () => _showDeleteAllConfirmDialog(), + ), + ]; + } + + /// 构建弹出菜单项 + PopupMenuEntry _buildPopupMenuItem( + Icon icon, + String title, + Color color, + Function() onTap, + ) { + return PopupMenuItem( + onTap: onTap, + child: SizedBox( + width: 150.w, + child: Row( + children: [ + icon, + 8.horizontalSpace, + Text( + title, + style: TextStyle( + color: color, + fontSize: 14.sp, + ), + ), + ], + ), + ), + ); + } + + /// 复制翻译条目内容 + void _copyTranslationItem(TranslationItem item) { + // 构建复制文本:原文 + 翻译 + String copyText = item.sourceText; + if (item.translatedText.isNotEmpty) { + copyText += '\n${item.translatedText}'; + } + + Clipboard.setData(ClipboardData(text: copyText)).then((_) { + Get.snackbar( + 'success'.tr, + 'copySuccess'.tr, // 复制成功 + backgroundColor: Colors.blue.withOpacity(0.8), + colorText: Colors.white, + snackPosition: SnackPosition.BOTTOM, + duration: const Duration(milliseconds: 1200), + icon: Icon( + Icons.check_circle_outline, + color: Colors.white, + size: 24.sp, + ), + ); + }); + } + // 显示删除单条记录确认对话框 void _showDeleteItemConfirmDialog(TranslationItem item) { final isDarkMode = Theme.of(Get.context!).brightness == Brightness.dark; @@ -1006,6 +1180,159 @@ class TranslationView extends GetView { } } + // 显示删除所有记录确认对话框 + void _showDeleteAllConfirmDialog() { + final isDarkMode = Theme.of(Get.context!).brightness == Brightness.dark; + + Get.dialog( + AlertDialog( + backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white, + title: Text( + '删除所有翻译历史', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.bold, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.warning_amber_rounded, + color: Colors.orange, + size: 24.sp, + ), + SizedBox(width: 8.w), + Expanded( + child: Text( + '危险操作', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.bold, + color: Colors.orange, + ), + ), + ), + ], + ), + SizedBox(height: 12.h), + Text( + '确定要删除所有翻译历史记录吗?此操作将清空所有翻译记录,且无法撤销。', + style: TextStyle( + fontSize: 16.sp, + color: isDarkMode ? Colors.grey[300] : Colors.black87, + height: 1.4, + ), + ), + SizedBox(height: 12.h), + Container( + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + color: Colors.red.withOpacity(0.1), + borderRadius: BorderRadius.circular(8.r), + border: Border.all( + color: Colors.red.withOpacity(0.3), + width: 1, + ), + ), + child: Row( + children: [ + Icon( + Icons.info_outline, + color: Colors.red, + size: 20.sp, + ), + SizedBox(width: 8.w), + Expanded( + child: Text( + '当前共有 ${controller.translationHistory.length} 条翻译记录', + style: TextStyle( + fontSize: 14.sp, + color: Colors.red[700], + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: Text( + 'cancel'.tr, + style: TextStyle( + fontSize: 16.sp, + color: isDarkMode ? Colors.grey[400] : Colors.grey[700], + ), + ), + ), + TextButton( + onPressed: () { + Get.back(); + _deleteAllTranslationHistory(); + }, + child: Text( + '确认删除', + style: TextStyle( + fontSize: 16.sp, + color: Colors.red, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.r), + ), + ), + ); + } + + // 删除所有翻译历史记录的具体实现 + void _deleteAllTranslationHistory() { + try { + // 调用控制器的删除所有记录方法 + controller.clearTranslationHistory(); + + // 显示删除成功提示 + Get.snackbar( + '删除成功', + '已清空所有翻译历史记录', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Get.isDarkMode ? Colors.grey[800] : Colors.grey[200], + colorText: Get.isDarkMode ? Colors.white : Colors.black, + duration: const Duration(milliseconds: 1200), + icon: Icon( + Icons.check_circle_outline, + color: Colors.green, + size: 24.sp, + ), + ); + } catch (e) { + // 显示删除失败提示 + Get.snackbar( + 'error'.tr, + '删除翻译历史时发生错误: ${e.toString()}', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red[400], + colorText: Colors.white, + duration: const Duration(milliseconds: 1500), + icon: Icon( + Icons.error_outline, + color: Colors.white, + size: 24.sp, + ), + ); + } + } + // 构建语言选择栏 Widget _buildLanguageSelectionBar(bool isDarkMode) { return Container(