From baeeff8da9b0ba8673fe41162e1d2e9bcad65617 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Thu, 17 Jul 2025 16:09:56 +0800 Subject: [PATCH 01/12] =?UTF-8?q?refactor(translation):=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E8=AF=AD=E8=A8=80=E9=80=89=E6=8B=A9=E5=AF=B9=E8=AF=9D?= =?UTF-8?q?=E6=A1=86=E7=9A=84UI=E5=92=8C=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用GestureDetector替代ListTile提升点击体验 - 统一圆角尺寸和内边距保持设计一致性 - 改进选中状态视觉反馈,增加背景色和边框 - 优化暗黑模式下的颜色对比度 - 简化代码结构,移除冗余容器 --- .../translation/views/translation_view.dart | 201 +++++++++++------- 1 file changed, 126 insertions(+), 75 deletions(-) diff --git a/lib/modules/translation/views/translation_view.dart b/lib/modules/translation/views/translation_view.dart index ed9d193eb..e9797d055 100644 --- a/lib/modules/translation/views/translation_view.dart +++ b/lib/modules/translation/views/translation_view.dart @@ -668,61 +668,49 @@ class TranslationView extends GetView { // 显示语言选择对话框 void _showLanguageSelectionDialog(bool isSource, bool isDarkMode) { - Get.dialog( - Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(24.r), - ), - insetPadding: EdgeInsets.symmetric(horizontal: 20.w), - backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white, - child: Container( - width: 0.9.sw, - padding: EdgeInsets.only(top: 32.h, bottom: 16.h), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.symmetric(horizontal: 24.w), - child: Text( - isSource - ? 'selectSourceLanguage'.tr // 选择源语言 - : 'selectTargetLanguage'.tr, // 选择目标语言 - style: TextStyle( - fontSize: 20.sp, - fontWeight: FontWeight.bold, - color: isDarkMode ? Colors.white : Colors.black, - ), + Get.dialog( + Dialog( + backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.r), // 统一圆角尺寸为16 + ), + insetPadding: EdgeInsets.symmetric(horizontal: 20.w), + child: Padding( // 使用Padding替代Container的padding + padding: EdgeInsets.all(12.w), // 统一内边距 + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 标题区域 + Padding( + padding: EdgeInsets.symmetric(horizontal: 8.w), + child: Text( + isSource + ? '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: - controller.supportedLanguages.keys.map((language) { - return ListTile( - title: Text( - '${language}'.tr, - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - trailing: Obx(() { - final currentLanguage = isSource - ? controller.sourceLanguage.value - : controller.targetLanguage.value; - return currentLanguage == language - ? Icon( - Icons.check_circle, - color: isDarkMode - ? AppColors.primary - : Colors.blue, - ) - : const SizedBox.shrink(); - }), + ), + SizedBox(height: 16.h), + + // 语言列表区域 + Container( + constraints: BoxConstraints(maxHeight: 0.5.sh), + 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; + + return GestureDetector( // 改用手势检测器实现点击 onTap: () { if (isSource) { controller.setSourceLanguage(language); @@ -731,34 +719,97 @@ class TranslationView extends GetView { } 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( + language.tr, + 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, + ), + ], + ), + ), ); - }).toList(), - ), + }); + }).toList(), ), ), - SizedBox(height: 16.h), - Align( - alignment: Alignment.centerRight, - child: Padding( - padding: EdgeInsets.only(right: 24.w), - child: TextButton( - onPressed: () => Get.back(), - child: Text( - 'cancel'.tr, // 取消 - style: TextStyle( - color: isDarkMode ? AppColors.primary : Colors.blue, - fontSize: 16.sp, - ), - ), + ), + 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, ), ), ), - ], - ), + ), + ], ), ), - ); - } + ), + ); +} // 构建加载更多指示器 Widget _buildLoadMoreIndicator() { @@ -979,7 +1030,7 @@ class TranslationView extends GetView { // 显示删除成功提示 Get.snackbar( - '删除成功', // deleteSuccess + 'success'.tr, // deleteSuccess '已删除该翻译记录', // deleteTranslationSuccess snackPosition: SnackPosition.BOTTOM, backgroundColor: Get.isDarkMode ? Colors.grey[800] : Colors.grey[200], @@ -989,7 +1040,7 @@ class TranslationView extends GetView { } catch (e) { // 显示删除失败提示 Get.snackbar( - '删除失败', // deleteFailed + 'error'.tr, // deleteFailed '删除翻译记录时发生错误: ${e.toString()}', // deleteTranslationError snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.red[400], From e9ad0a9968ac4549289bdf89fff1071e8e57409f Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Fri, 18 Jul 2025 11:24:37 +0800 Subject: [PATCH 02/12] =?UTF-8?q?style(translation):=20=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E6=B3=A8=E9=87=8A=E9=94=99=E5=88=AB=E5=AD=97=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E4=BB=A3=E7=A0=81=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将“需要是开启”改为“需要时开启” - 移除多余空行 - 统一代码缩进和换行格式 --- .../views/translation_detail_view.dart | 2 +- .../translation/views/translation_view.dart | 271 +++++++++--------- 2 files changed, 140 insertions(+), 133 deletions(-) diff --git a/lib/modules/translation/views/translation_detail_view.dart b/lib/modules/translation/views/translation_detail_view.dart index afe3e668d..629348420 100644 --- a/lib/modules/translation/views/translation_detail_view.dart +++ b/lib/modules/translation/views/translation_detail_view.dart @@ -110,7 +110,7 @@ class TranslationDetailView extends GetView { isDarkMode: isDarkMode, )), ), - //屏蔽“记录摘要”,需要是开启 + //屏蔽“记录摘要”,需要时开启 // SizedBox(width: 12.w), // Expanded( // child: Obx(() => _buildTabButton( diff --git a/lib/modules/translation/views/translation_view.dart b/lib/modules/translation/views/translation_view.dart index e9797d055..5420854d5 100644 --- a/lib/modules/translation/views/translation_view.dart +++ b/lib/modules/translation/views/translation_view.dart @@ -109,7 +109,6 @@ class TranslationView extends GetView { )), centerTitle: true, actions: [ - Obx(() { if (controller.isRecording.value) { // 录音中:显示红色计时器样式,点击可停止录音 @@ -305,7 +304,8 @@ class TranslationView extends GetView { itemCount: itemCount, itemBuilder: (context, index) { // 如果是最后一项且有更多历史记录,显示加载指示器(reverse模式下) - if (index == controller.translationHistory.length && controller.hasMoreHistory.value) { + if (index == controller.translationHistory.length && + controller.hasMoreHistory.value) { return _buildLoadMoreIndicator(); } @@ -313,7 +313,8 @@ class TranslationView extends GetView { final historyIndex = controller.translationHistory.length - 1 - index; // 检查索引是否有效(reverse模式下的索引验证) - if (historyIndex < 0 || historyIndex >= controller.translationHistory.length) { + if (historyIndex < 0 || + historyIndex >= controller.translationHistory.length) { return const SizedBox.shrink(); } @@ -668,148 +669,153 @@ class TranslationView extends GetView { // 显示语言选择对话框 void _showLanguageSelectionDialog(bool isSource, bool isDarkMode) { - Get.dialog( - Dialog( - backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16.r), // 统一圆角尺寸为16 - ), - insetPadding: EdgeInsets.symmetric(horizontal: 20.w), - child: Padding( // 使用Padding替代Container的padding - padding: EdgeInsets.all(12.w), // 统一内边距 - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 标题区域 - Padding( - padding: EdgeInsets.symmetric(horizontal: 8.w), - child: Text( - isSource - ? 'selectSourceLanguage'.tr - : 'selectTargetLanguage'.tr, // 保留源语言/目标语言区分 - style: TextStyle( - fontSize: 18.sp, // 调整字号 - fontWeight: FontWeight.w600, // 使用数字字重 - color: isDarkMode ? Colors.white : Colors.black87, // 优化文本颜色 + Get.dialog( + Dialog( + backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.r), // 统一圆角尺寸为16 + ), + insetPadding: EdgeInsets.symmetric(horizontal: 20.w), + child: Padding( + // 使用Padding替代Container的padding + padding: EdgeInsets.all(12.w), // 统一内边距 + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 标题区域 + Padding( + padding: EdgeInsets.symmetric(horizontal: 8.w), + child: Text( + isSource + ? '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: controller.supportedLanguages.keys.map((language) { - return Obx(() { - final currentLanguage = isSource - ? controller.sourceLanguage.value - : controller.targetLanguage.value; - final isSelected = currentLanguage == language; - - return GestureDetector( // 改用手势检测器实现点击 - onTap: () { - if (isSource) { - controller.setSourceLanguage(language); - } else { - controller.setTargetLanguage(language); - } - 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, + SizedBox(height: 16.h), + + // 语言列表区域 + Container( + constraints: BoxConstraints(maxHeight: 0.5.sh), + 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; + + return GestureDetector( + // 改用手势检测器实现点击 + onTap: () { + if (isSource) { + controller.setSourceLanguage(language); + } else { + controller.setTargetLanguage(language); + } + 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( - language.tr, - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w500, - color: isDarkMode - ? Colors.white - : Colors.black87, - overflow: TextOverflow.ellipsis, + child: Row( + children: [ + // 语言名称 + Expanded( + child: Text( + language.tr, + 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, // 圆形样式 + // 选中指示器 + 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, ), - child: isSelected - ? Icon( // 选中时显示对勾 - Icons.check, - size: 16.w, - color: Colors.white, - ) - : null, - ), - ], + ], + ), ), - ), - ); - }); - }).toList(), + ); + }); + }).toList(), + ), ), ), - ), - 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, + 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, + ), ), ), ), - ), - ], + ], + ), ), ), - ), - ); -} + ); + } // 构建加载更多指示器 Widget _buildLoadMoreIndicator() { @@ -853,7 +859,8 @@ class TranslationView extends GetView { child: GestureDetector( onTap: () => controller.loadMoreHistory(), child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: Colors.red[50], borderRadius: BorderRadius.circular(20), From 1309eaf060ab9ab7ce86966e44fbfd2e972ba712 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Fri, 18 Jul 2025 11:25:00 +0800 Subject: [PATCH 03/12] =?UTF-8?q?feat(translation):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E7=BF=BB=E8=AF=91=E4=BD=BF=E7=94=A8=E7=BB=9F=E8=AE=A1=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加翻译统计页面及相关路由配置 - 实现翻译、ASR、TTS API调用统计功能 - 新增成本计算和预警机制 - 完善统计数据的存储和展示逻辑 --- lib/data/models/translation_usage_stats.dart | 450 +++++++++++ .../services/translation_usage_service.dart | 519 ++++++++++++ lib/modules/settings/views/settings_view.dart | 23 + .../bindings/translation_stats_binding.dart | 18 + .../controllers/translation_controller.dart | 162 +++- .../translation_cost_controller.dart | 258 ++++++ .../translation_detail_controller.dart | 25 +- .../translation_stats_controller.dart | 358 +++++++++ .../views/translation_stats_view.dart | 757 ++++++++++++++++++ lib/routes/app_pages.dart | 9 + lib/routes/app_routes.dart | 1 + 11 files changed, 2570 insertions(+), 10 deletions(-) create mode 100644 lib/data/models/translation_usage_stats.dart create mode 100644 lib/data/services/translation_usage_service.dart create mode 100644 lib/modules/translation/bindings/translation_stats_binding.dart create mode 100644 lib/modules/translation/controllers/translation_cost_controller.dart create mode 100644 lib/modules/translation/controllers/translation_stats_controller.dart create mode 100644 lib/modules/translation/views/translation_stats_view.dart diff --git a/lib/data/models/translation_usage_stats.dart b/lib/data/models/translation_usage_stats.dart new file mode 100644 index 000000000..6d73b1958 --- /dev/null +++ b/lib/data/models/translation_usage_stats.dart @@ -0,0 +1,450 @@ + + +/// 翻译模式统计数据 +class TranslationModeStats { + final int apiCalls; // API调用次数 + final int sourceCharacters; // 源文本字符数 + final int effectiveAudioDuration; // 有效音频时长 + final int ttsCharacters; // TTS字符数 + final double cost; // 成本 + + TranslationModeStats({ + this.apiCalls = 0, + this.sourceCharacters = 0, + this.effectiveAudioDuration = 0, + this.ttsCharacters = 0, + this.cost = 0.0, + }); + + factory TranslationModeStats.fromJson(Map json) { + return TranslationModeStats( + apiCalls: json['apiCalls'] ?? 0, + sourceCharacters: json['sourceCharacters'] ?? 0, + effectiveAudioDuration: json['effectiveAudioDuration'] ?? 0, + ttsCharacters: json['ttsCharacters'] ?? 0, + cost: (json['cost'] ?? 0.0).toDouble(), + ); + } + + Map toJson() { + return { + 'apiCalls': apiCalls, + 'sourceCharacters': sourceCharacters, + 'effectiveAudioDuration': effectiveAudioDuration, + 'ttsCharacters': ttsCharacters, + 'cost': cost, + }; + } + + TranslationModeStats copyWith({ + int? apiCalls, + int? sourceCharacters, + int? effectiveAudioDuration, + int? ttsCharacters, + double? cost, + }) { + return TranslationModeStats( + apiCalls: apiCalls ?? this.apiCalls, + sourceCharacters: sourceCharacters ?? this.sourceCharacters, + effectiveAudioDuration: effectiveAudioDuration ?? this.effectiveAudioDuration, + ttsCharacters: ttsCharacters ?? this.ttsCharacters, + cost: cost ?? this.cost, + ); + } +} + +/// 语言对统计数据 +class LanguagePairStats { + final String languagePair; // 语言对,如"zh-CN→en-US" + final int translationCount; // 翻译次数 + final int sourceCharacters; // 源文本字符数 + final double cost; // 成本 + + LanguagePairStats({ + required this.languagePair, + this.translationCount = 0, + this.sourceCharacters = 0, + this.cost = 0.0, + }); + + factory LanguagePairStats.fromJson(Map json) { + return LanguagePairStats( + languagePair: json['languagePair'] ?? '', + translationCount: json['translationCount'] ?? 0, + sourceCharacters: json['sourceCharacters'] ?? 0, + cost: (json['cost'] ?? 0.0).toDouble(), + ); + } + + Map toJson() { + return { + 'languagePair': languagePair, + 'translationCount': translationCount, + 'sourceCharacters': sourceCharacters, + 'cost': cost, + }; + } +} + +/// 小时统计数据 +class HourlyStats { + final int hour; // 小时(0-23) + final int apiCalls; // API调用次数 + final int sourceCharacters; // 源文本字符数 + final int effectiveAudioDuration; // 有效音频时长 + final double cost; // 成本 + + HourlyStats({ + required this.hour, + this.apiCalls = 0, + this.sourceCharacters = 0, + this.effectiveAudioDuration = 0, + this.cost = 0.0, + }); + + factory HourlyStats.fromJson(Map json) { + return HourlyStats( + hour: json['hour'] ?? 0, + apiCalls: json['apiCalls'] ?? 0, + sourceCharacters: json['sourceCharacters'] ?? 0, + effectiveAudioDuration: json['effectiveAudioDuration'] ?? 0, + cost: (json['cost'] ?? 0.0).toDouble(), + ); + } + + Map toJson() { + return { + 'hour': hour, + 'apiCalls': apiCalls, + 'sourceCharacters': sourceCharacters, + 'effectiveAudioDuration': effectiveAudioDuration, + 'cost': cost, + }; + } +} + +/// 翻译使用统计数据模型 - 优化版 +class TranslationUsageStats { + // 基础信息 + final String id; // 统计记录ID + final DateTime date; // 统计日期 + final String userId; // 用户ID + final String timeGranularity; // 时间粒度:realtime/hour/day/week/month + + // API调用统计 - 精确化 + final int translationApiCalls; // 翻译API调用次数 + final int asrApiCalls; // ASR语音识别调用次数 + final int ttsApiCalls; // TTS语音合成调用次数 + final int aiSummaryApiCalls; // AI摘要生成调用次数(已废弃但保留兼容性) + + // 精确计费参数统计 + final int sourceTextCharacters; // 源文本字符数(翻译API计费基础) + final int effectiveAudioDuration; // ASR有效语音时长(秒,ASR计费基础) + final int ttsTextCharacters; // TTS文本字符数(TTS计费基础) + final int totalSessionDuration; // 总会话时长(秒,用于分析) + + // 网络流量统计 + final int asrNetworkBytes; // ASR网络流量(字节) + final int ttsNetworkBytes; // TTS网络流量(字节) + final int translationNetworkBytes; // 翻译API网络流量(字节) + + // 多维度分类统计 + final int sessionCount; // 翻译会话数量 + final Map modeStats; // 按翻译模式分类统计 + final Map languagePairStats; // 按语言对分类统计 + final Map hourlyStats; // 按小时分类统计(仅日统计包含) + + // 成本相关 + final double estimatedCost; // 预估成本 + final String currency; // 货币单位 + final Map costBreakdown; // 成本分解(ASR/TTS/Translation) + + TranslationUsageStats({ + required this.id, + required this.date, + required this.userId, + this.timeGranularity = 'day', + this.translationApiCalls = 0, + this.asrApiCalls = 0, + this.ttsApiCalls = 0, + this.aiSummaryApiCalls = 0, + this.sourceTextCharacters = 0, + this.effectiveAudioDuration = 0, + this.ttsTextCharacters = 0, + this.totalSessionDuration = 0, + this.asrNetworkBytes = 0, + this.ttsNetworkBytes = 0, + this.translationNetworkBytes = 0, + this.sessionCount = 0, + this.modeStats = const {}, + this.languagePairStats = const {}, + this.hourlyStats = const {}, + this.estimatedCost = 0.0, + this.currency = 'CNY', + this.costBreakdown = const {}, + }); + + factory TranslationUsageStats.fromJson(Map json) { + // 处理多维度统计数据的反序列化 + final modeStatsMap = {}; + if (json['modeStats'] != null) { + final modeStatsJson = json['modeStats'] as Map; + modeStatsJson.forEach((key, value) { + modeStatsMap[key] = TranslationModeStats.fromJson(value); + }); + } + + final languagePairStatsMap = {}; + if (json['languagePairStats'] != null) { + final languagePairStatsJson = json['languagePairStats'] as Map; + languagePairStatsJson.forEach((key, value) { + languagePairStatsMap[key] = LanguagePairStats.fromJson(value); + }); + } + + final hourlyStatsMap = {}; + if (json['hourlyStats'] != null) { + final hourlyStatsJson = json['hourlyStats'] as Map; + hourlyStatsJson.forEach((key, value) { + hourlyStatsMap[key] = HourlyStats.fromJson(value); + }); + } + + return TranslationUsageStats( + id: json['id'] ?? '', + date: DateTime.parse(json['date']), + userId: json['userId'] ?? '', + timeGranularity: json['timeGranularity'] ?? 'day', + translationApiCalls: json['translationApiCalls'] ?? 0, + asrApiCalls: json['asrApiCalls'] ?? 0, + ttsApiCalls: json['ttsApiCalls'] ?? 0, + aiSummaryApiCalls: json['aiSummaryApiCalls'] ?? 0, + sourceTextCharacters: json['sourceTextCharacters'] ?? json['sourceCharacterCount'] ?? 0, // 兼容旧字段 + effectiveAudioDuration: json['effectiveAudioDuration'] ?? json['audioRecordingDuration'] ?? 0, // 兼容旧字段 + ttsTextCharacters: json['ttsTextCharacters'] ?? json['targetCharacterCount'] ?? 0, // 兼容旧字段 + totalSessionDuration: json['totalSessionDuration'] ?? 0, + asrNetworkBytes: json['asrNetworkBytes'] ?? 0, + ttsNetworkBytes: json['ttsNetworkBytes'] ?? 0, + translationNetworkBytes: json['translationNetworkBytes'] ?? 0, + sessionCount: json['sessionCount'] ?? 0, + modeStats: modeStatsMap, + languagePairStats: languagePairStatsMap, + hourlyStats: hourlyStatsMap, + estimatedCost: (json['estimatedCost'] ?? 0.0).toDouble(), + currency: json['currency'] ?? 'CNY', + costBreakdown: Map.from(json['costBreakdown'] ?? {}), + ); + } + + Map toJson() { + // 序列化多维度统计数据 + final modeStatsJson = {}; + modeStats.forEach((key, value) { + modeStatsJson[key] = value.toJson(); + }); + + final languagePairStatsJson = {}; + languagePairStats.forEach((key, value) { + languagePairStatsJson[key] = value.toJson(); + }); + + final hourlyStatsJson = {}; + hourlyStats.forEach((key, value) { + hourlyStatsJson[key] = value.toJson(); + }); + + return { + 'id': id, + 'date': date.toIso8601String(), + 'userId': userId, + 'timeGranularity': timeGranularity, + 'translationApiCalls': translationApiCalls, + 'asrApiCalls': asrApiCalls, + 'ttsApiCalls': ttsApiCalls, + 'aiSummaryApiCalls': aiSummaryApiCalls, + 'sourceTextCharacters': sourceTextCharacters, + 'effectiveAudioDuration': effectiveAudioDuration, + 'ttsTextCharacters': ttsTextCharacters, + 'totalSessionDuration': totalSessionDuration, + 'asrNetworkBytes': asrNetworkBytes, + 'ttsNetworkBytes': ttsNetworkBytes, + 'translationNetworkBytes': translationNetworkBytes, + 'sessionCount': sessionCount, + 'modeStats': modeStatsJson, + 'languagePairStats': languagePairStatsJson, + 'hourlyStats': hourlyStatsJson, + 'estimatedCost': estimatedCost, + 'currency': currency, + 'costBreakdown': costBreakdown, + // 保持向后兼容性 + 'sourceCharacterCount': sourceTextCharacters, + 'targetCharacterCount': ttsTextCharacters, + 'audioRecordingDuration': effectiveAudioDuration, + 'ttsPlaybackDuration': totalSessionDuration, + }; + } + + /// 创建空的统计记录 + factory TranslationUsageStats.empty(String userId) { + return TranslationUsageStats( + id: DateTime.now().millisecondsSinceEpoch.toString(), + date: DateTime.now(), + userId: userId, + ); + } + + /// 复制并更新统计数据 + TranslationUsageStats copyWith({ + String? id, + DateTime? date, + String? userId, + String? timeGranularity, + int? translationApiCalls, + int? asrApiCalls, + int? ttsApiCalls, + int? aiSummaryApiCalls, + int? sourceTextCharacters, + int? effectiveAudioDuration, + int? ttsTextCharacters, + int? totalSessionDuration, + int? asrNetworkBytes, + int? ttsNetworkBytes, + int? translationNetworkBytes, + int? sessionCount, + Map? modeStats, + Map? languagePairStats, + Map? hourlyStats, + double? estimatedCost, + String? currency, + Map? costBreakdown, + }) { + return TranslationUsageStats( + id: id ?? this.id, + date: date ?? this.date, + userId: userId ?? this.userId, + timeGranularity: timeGranularity ?? this.timeGranularity, + translationApiCalls: translationApiCalls ?? this.translationApiCalls, + asrApiCalls: asrApiCalls ?? this.asrApiCalls, + ttsApiCalls: ttsApiCalls ?? this.ttsApiCalls, + aiSummaryApiCalls: aiSummaryApiCalls ?? this.aiSummaryApiCalls, + sourceTextCharacters: sourceTextCharacters ?? this.sourceTextCharacters, + effectiveAudioDuration: effectiveAudioDuration ?? this.effectiveAudioDuration, + ttsTextCharacters: ttsTextCharacters ?? this.ttsTextCharacters, + totalSessionDuration: totalSessionDuration ?? this.totalSessionDuration, + asrNetworkBytes: asrNetworkBytes ?? this.asrNetworkBytes, + ttsNetworkBytes: ttsNetworkBytes ?? this.ttsNetworkBytes, + translationNetworkBytes: translationNetworkBytes ?? this.translationNetworkBytes, + sessionCount: sessionCount ?? this.sessionCount, + modeStats: modeStats ?? this.modeStats, + languagePairStats: languagePairStats ?? this.languagePairStats, + hourlyStats: hourlyStats ?? this.hourlyStats, + estimatedCost: estimatedCost ?? this.estimatedCost, + currency: currency ?? this.currency, + costBreakdown: costBreakdown ?? this.costBreakdown, + ); + } + + /// 获取总API调用次数 + int get totalApiCalls => translationApiCalls + asrApiCalls + ttsApiCalls + aiSummaryApiCalls; + + /// 获取总字符数(精确计费基础) + int get totalCharacterCount => sourceTextCharacters + ttsTextCharacters; + + /// 获取总音频时长(包含有效时长和总会话时长) + int get totalAudioDuration => effectiveAudioDuration + totalSessionDuration; + + /// 获取总网络流量 + int get totalNetworkBytes => asrNetworkBytes + ttsNetworkBytes + translationNetworkBytes; + + /// 获取最常用的语言对 + String get mostUsedLanguagePair { + if (languagePairStats.isEmpty) return '无数据'; + return languagePairStats.entries + .reduce((a, b) => a.value.translationCount > b.value.translationCount ? a : b) + .key; + } + + /// 获取最常用的翻译模式 + String get mostUsedMode { + if (modeStats.isEmpty) return '无数据'; + return modeStats.entries + .reduce((a, b) => a.value.apiCalls > b.value.apiCalls ? a : b) + .key; + } + + // 向后兼容性getter + /// @deprecated 使用 sourceTextCharacters 替代 + int get sourceCharacterCount => sourceTextCharacters; + + /// @deprecated 使用 ttsTextCharacters 替代 + int get targetCharacterCount => ttsTextCharacters; + + /// @deprecated 使用 effectiveAudioDuration 替代 + int get audioRecordingDuration => effectiveAudioDuration; + + /// @deprecated 使用 totalSessionDuration 替代 + int get ttsPlaybackDuration => totalSessionDuration; + + /// @deprecated 使用 languagePairStats 替代 + Map get languagePairUsage { + final result = {}; + languagePairStats.forEach((key, value) { + result[key] = value.translationCount; + }); + return result; + } + + /// @deprecated 使用 modeStats 替代 + Map get modeUsage { + final result = {}; + modeStats.forEach((key, value) { + result[key] = value.apiCalls; + }); + return result; + } +} + +/// 实时统计数据模型(用于当前会话)- 优化版 +class RealtimeUsageStats { + // 精确计费参数 + int translationCount = 0; // 当前会话翻译次数 + int sourceTextCharacters = 0; // 当前会话源文本字符数(翻译计费基础) + int ttsTextCharacters = 0; // 当前会话TTS文本字符数(TTS计费基础) + int effectiveAudioDuration = 0; // 当前会话有效语音时长(ASR计费基础) + int totalSessionDuration = 0; // 当前会话总时长 + + // 会话信息 + String currentLanguagePair = ''; // 当前语言对 + String currentMode = ''; // 当前翻译模式 + DateTime sessionStartTime = DateTime.now(); // 会话开始时间 + + /// 重置统计数据 + void reset() { + translationCount = 0; + sourceTextCharacters = 0; + ttsTextCharacters = 0; + effectiveAudioDuration = 0; + totalSessionDuration = 0; + currentLanguagePair = ''; + currentMode = ''; + sessionStartTime = DateTime.now(); + } + + /// 获取会话持续时间(分钟) + int get sessionDurationMinutes { + return DateTime.now().difference(sessionStartTime).inMinutes; + } + + // 向后兼容性getter + /// @deprecated 使用 sourceTextCharacters 替代 + int get sourceCharacters => sourceTextCharacters; + + /// @deprecated 使用 ttsTextCharacters 替代 + int get targetCharacters => ttsTextCharacters; + + /// @deprecated 使用 effectiveAudioDuration 替代 + int get recordingDuration => effectiveAudioDuration; + + /// @deprecated 使用 totalSessionDuration 替代 + int get ttsPlayDuration => totalSessionDuration; +} diff --git a/lib/data/services/translation_usage_service.dart b/lib/data/services/translation_usage_service.dart new file mode 100644 index 000000000..90f3db799 --- /dev/null +++ b/lib/data/services/translation_usage_service.dart @@ -0,0 +1,519 @@ +import 'dart:convert'; +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; +import '../models/translation_usage_stats.dart'; +import '../../core/utils/logger.dart'; + +/// 翻译使用量统计服务 +class TranslationUsageService extends GetxService { + static const String _storageKey = 'translation_usage_stats'; + static const String _dailyStatsKey = 'daily_translation_stats'; + static const String _costConfigKey = 'translation_cost_config'; + + final GetStorage _storage = GetStorage(); + + // 实时统计数据 + final Rx realtimeStats = RealtimeUsageStats().obs; + + // 当日统计数据 + final Rx todayStats = TranslationUsageStats.empty('').obs; + + // 成本配置 + final RxMap costConfig = {}.obs; + + // 预警阈值 + final RxDouble dailyCostThreshold = 10.0.obs; // 日成本预警阈值 + final RxDouble monthlyCostThreshold = 300.0.obs; // 月成本预警阈值 + + @override + void onInit() { + super.onInit(); + _initializeService(); + } + + /// 初始化服务 + void _initializeService() { + _loadCostConfig(); + _loadTodayStats(); + Logger.info('翻译使用量统计服务初始化完成'); + } + + /// 加载成本配置 + void _loadCostConfig() { + try { + final configData = _storage.read(_costConfigKey); + if (configData != null) { + costConfig.value = Map.from(configData); + } else { + // 设置默认成本配置(每千字符的成本,单位:元) + costConfig.value = { + 'translation_per_1k_chars': 0.05, // 翻译:每千字符0.05元 + 'asr_per_minute': 0.02, // ASR:每分钟0.02元 + 'tts_per_1k_chars': 0.03, // TTS:每千字符0.03元 + 'ai_summary_per_request': 0.01, // AI摘要:每次请求0.01元 + }; + _saveCostConfig(); + } + } catch (e) { + Logger.error('加载成本配置失败: $e'); + } + } + + /// 保存成本配置 + void _saveCostConfig() { + try { + _storage.write(_costConfigKey, costConfig.value); + } catch (e) { + Logger.error('保存成本配置失败: $e'); + } + } + + /// 加载当日统计数据 + void _loadTodayStats() { + try { + final today = DateTime.now(); + final todayKey = '${today.year}-${today.month.toString().padLeft(2, '0')}-${today.day.toString().padLeft(2, '0')}'; + + final statsData = _storage.read('${_dailyStatsKey}_$todayKey'); + if (statsData != null) { + todayStats.value = TranslationUsageStats.fromJson(Map.from(statsData)); + } else { + todayStats.value = TranslationUsageStats.empty(getCurrentUserId()); + } + } catch (e) { + Logger.error('加载当日统计数据失败: $e'); + todayStats.value = TranslationUsageStats.empty(getCurrentUserId()); + } + } + + /// 获取当前用户ID + String getCurrentUserId() { + try { + final userInfo = _storage.read('user_info'); + if (userInfo != null && userInfo['user'] != null) { + return userInfo['user']['uid'] ?? 'anonymous'; + } + } catch (e) { + Logger.warning('获取用户ID失败: $e'); + } + return 'anonymous'; + } + + /// 记录翻译API调用 + void recordTranslationApiCall({ + required String sourceText, + required String targetText, + required String sourceLanguage, + required String targetLanguage, + required String mode, + }) { + try { + // 更新实时统计 + realtimeStats.value.translationCount++; + realtimeStats.value.sourceTextCharacters += sourceText.length; + realtimeStats.value.ttsTextCharacters += targetText.length; + realtimeStats.value.currentLanguagePair = '$sourceLanguage-$targetLanguage'; + realtimeStats.value.currentMode = mode; + realtimeStats.refresh(); + + // 更新当日统计 - 使用多维度统计 + final languagePair = '$sourceLanguage→$targetLanguage'; + + // 更新语言对统计 + final newLanguagePairStats = Map.from(todayStats.value.languagePairStats); + if (newLanguagePairStats.containsKey(languagePair)) { + final existing = newLanguagePairStats[languagePair]!; + newLanguagePairStats[languagePair] = LanguagePairStats( + languagePair: languagePair, + translationCount: existing.translationCount + 1, + sourceCharacters: existing.sourceCharacters + sourceText.length, + cost: existing.cost + _calculateTranslationCost(sourceText.length), + ); + } else { + newLanguagePairStats[languagePair] = LanguagePairStats( + languagePair: languagePair, + translationCount: 1, + sourceCharacters: sourceText.length, + cost: _calculateTranslationCost(sourceText.length), + ); + } + + // 更新模式统计 + final newModeStats = Map.from(todayStats.value.modeStats); + if (newModeStats.containsKey(mode)) { + final existing = newModeStats[mode]!; + newModeStats[mode] = existing.copyWith( + apiCalls: existing.apiCalls + 1, + sourceCharacters: existing.sourceCharacters + sourceText.length, + ttsCharacters: existing.ttsCharacters + targetText.length, + cost: existing.cost + _calculateTranslationCost(sourceText.length), + ); + } else { + newModeStats[mode] = TranslationModeStats( + apiCalls: 1, + sourceCharacters: sourceText.length, + ttsCharacters: targetText.length, + cost: _calculateTranslationCost(sourceText.length), + ); + } + + todayStats.value = todayStats.value.copyWith( + translationApiCalls: todayStats.value.translationApiCalls + 1, + sourceTextCharacters: todayStats.value.sourceTextCharacters + sourceText.length, + ttsTextCharacters: todayStats.value.ttsTextCharacters + targetText.length, + languagePairStats: newLanguagePairStats, + modeStats: newModeStats, + estimatedCost: _calculateEstimatedCost(todayStats.value.copyWith( + translationApiCalls: todayStats.value.translationApiCalls + 1, + sourceTextCharacters: todayStats.value.sourceTextCharacters + sourceText.length, + ttsTextCharacters: todayStats.value.ttsTextCharacters + targetText.length, + )), + ); + + _saveTodayStats(); + _checkCostThreshold(); + + Logger.info('记录翻译API调用: 源文本${sourceText.length}字符, 目标文本${targetText.length}字符'); + } catch (e) { + Logger.error('记录翻译API调用失败: $e'); + } + } + + /// 记录ASR API调用 - 优化版(精确计费参数) + void recordAsrApiCall({ + required int effectiveDurationSeconds, // 有效语音时长(VAD检测的实际语音) + required int totalDurationSeconds, // 总会话时长(包含静音) + required String language, + required String mode, // 翻译模式 + String? audioFormat, // 音频格式(可选) + int? sampleRate, // 采样率(可选) + }) { + try { + // 更新实时统计(使用有效语音时长作为主要计费依据) + realtimeStats.value.effectiveAudioDuration += effectiveDurationSeconds; + realtimeStats.value.totalSessionDuration += totalDurationSeconds; + realtimeStats.refresh(); + + // 更新模式统计 + final newModeStats = Map.from(todayStats.value.modeStats); + if (newModeStats.containsKey(mode)) { + final existing = newModeStats[mode]!; + newModeStats[mode] = existing.copyWith( + effectiveAudioDuration: existing.effectiveAudioDuration + effectiveDurationSeconds, + cost: existing.cost + _calculateAsrCost(effectiveDurationSeconds), + ); + } else { + newModeStats[mode] = TranslationModeStats( + effectiveAudioDuration: effectiveDurationSeconds, + cost: _calculateAsrCost(effectiveDurationSeconds), + ); + } + + // 更新当日统计 + todayStats.value = todayStats.value.copyWith( + asrApiCalls: todayStats.value.asrApiCalls + 1, + effectiveAudioDuration: todayStats.value.effectiveAudioDuration + effectiveDurationSeconds, + totalSessionDuration: todayStats.value.totalSessionDuration + totalDurationSeconds, + modeStats: newModeStats, + estimatedCost: _calculateEstimatedCost(todayStats.value.copyWith( + asrApiCalls: todayStats.value.asrApiCalls + 1, + effectiveAudioDuration: todayStats.value.effectiveAudioDuration + effectiveDurationSeconds, + )), + ); + + _saveTodayStats(); + + Logger.info('ASR统计: 有效语音${effectiveDurationSeconds}秒, 总时长${totalDurationSeconds}秒, 语言$language, 模式$mode'); + } catch (e) { + Logger.error('记录ASR API调用失败: $e'); + } + } + + /// 记录TTS API调用 + void recordTtsApiCall({ + required String text, + required int durationSeconds, + required String language, + }) { + try { + // 更新实时统计 + realtimeStats.value.totalSessionDuration += durationSeconds; + realtimeStats.refresh(); + + // 更新当日统计(TTS按字符数计费,不是按时长) + todayStats.value = todayStats.value.copyWith( + ttsApiCalls: todayStats.value.ttsApiCalls + 1, + ttsTextCharacters: todayStats.value.ttsTextCharacters + text.length, + totalSessionDuration: todayStats.value.totalSessionDuration + durationSeconds, + estimatedCost: _calculateEstimatedCost(todayStats.value.copyWith( + ttsApiCalls: todayStats.value.ttsApiCalls + 1, + ttsTextCharacters: todayStats.value.ttsTextCharacters + text.length, + )), + ); + + _saveTodayStats(); + + Logger.info('记录TTS API调用: 文本${text.length}字符, 时长${durationSeconds}秒'); + } catch (e) { + Logger.error('记录TTS API调用失败: $e'); + } + } + + /// 记录AI摘要API调用 + void recordAiSummaryApiCall() { + try { + // 更新当日统计 + todayStats.value = todayStats.value.copyWith( + aiSummaryApiCalls: todayStats.value.aiSummaryApiCalls + 1, + estimatedCost: _calculateEstimatedCost(todayStats.value.copyWith( + aiSummaryApiCalls: todayStats.value.aiSummaryApiCalls + 1, + )), + ); + + _saveTodayStats(); + + Logger.info('记录AI摘要API调用'); + } catch (e) { + Logger.error('记录AI摘要API调用失败: $e'); + } + } + + /// 开始新会话 + void startNewSession() { + realtimeStats.value.reset(); + + // 更新当日会话数 + todayStats.value = todayStats.value.copyWith( + sessionCount: todayStats.value.sessionCount + 1, + ); + + _saveTodayStats(); + Logger.info('开始新的翻译会话'); + } + + /// 计算预估成本 - 优化版(基于精确计费参数) + double _calculateEstimatedCost(TranslationUsageStats stats) { + double cost = 0.0; + + // 翻译成本:基于源文本字符数 + cost += (stats.sourceTextCharacters / 1000) * (costConfig['translation_per_1k_chars'] ?? 0.05); + + // ASR成本:基于有效音频时长 + cost += (stats.effectiveAudioDuration / 60) * (costConfig['asr_per_minute'] ?? 0.02); + + // TTS成本:基于TTS文本字符数 + cost += (stats.ttsTextCharacters / 1000) * (costConfig['tts_per_1k_chars'] ?? 0.03); + + // AI摘要成本(已废弃但保留兼容性) + cost += stats.aiSummaryApiCalls * (costConfig['ai_summary_per_request'] ?? 0.01); + + return cost; + } + + /// 计算翻译成本(单次调用) + double _calculateTranslationCost(int sourceCharacters) { + return (sourceCharacters / 1000) * (costConfig['translation_per_1k_chars'] ?? 0.05); + } + + /// 计算ASR成本(单次调用) + double _calculateAsrCost(int durationSeconds) { + return (durationSeconds / 60) * (costConfig['asr_per_minute'] ?? 0.02); + } + + /// 计算TTS成本(单次调用) + double _calculateTtsCost(int textCharacters) { + return (textCharacters / 1000) * (costConfig['tts_per_1k_chars'] ?? 0.03); + } + + /// 保存当日统计数据 + void _saveTodayStats() { + try { + final today = DateTime.now(); + final todayKey = '${today.year}-${today.month.toString().padLeft(2, '0')}-${today.day.toString().padLeft(2, '0')}'; + _storage.write('${_dailyStatsKey}_$todayKey', todayStats.value.toJson()); + } catch (e) { + Logger.error('保存当日统计数据失败: $e'); + } + } + + /// 检查成本阈值 + void _checkCostThreshold() { + final todayCost = todayStats.value.estimatedCost; + + if (todayCost >= dailyCostThreshold.value) { + _showCostWarning('今日翻译成本已达到预警阈值:¥${todayCost.toStringAsFixed(2)}'); + } + } + + /// 显示成本预警 + void _showCostWarning(String message) { + Get.snackbar( + '成本预警', + message, + snackPosition: SnackPosition.TOP, + backgroundColor: Get.theme.colorScheme.error.withOpacity(0.1), + colorText: Get.theme.colorScheme.error, + duration: const Duration(seconds: 5), + ); + } + + /// 获取历史统计数据 + Future> getHistoryStats({ + required DateTime startDate, + required DateTime endDate, + }) async { + try { + final List historyStats = []; + + for (DateTime date = startDate; + date.isBefore(endDate) || date.isAtSameMomentAs(endDate); + date = date.add(const Duration(days: 1))) { + + final dateKey = '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; + final statsData = _storage.read('${_dailyStatsKey}_$dateKey'); + + if (statsData != null) { + historyStats.add(TranslationUsageStats.fromJson(Map.from(statsData))); + } + } + + return historyStats; + } catch (e) { + Logger.error('获取历史统计数据失败: $e'); + return []; + } + } + + /// 清理过期数据(保留最近90天) + void cleanupOldData() { + try { + final cutoffDate = DateTime.now().subtract(const Duration(days: 90)); + final allKeys = _storage.getKeys(); + + for (final key in allKeys) { + if (key.startsWith(_dailyStatsKey)) { + final dateStr = key.replaceFirst('${_dailyStatsKey}_', ''); + try { + final date = DateTime.parse(dateStr); + if (date.isBefore(cutoffDate)) { + _storage.remove(key); + Logger.info('清理过期统计数据: $key'); + } + } catch (e) { + // 忽略解析错误的键 + } + } + } + } catch (e) { + Logger.error('清理过期数据失败: $e'); + } + } + + /// 更新成本配置 + void updateCostConfig(Map newConfig) { + costConfig.value = newConfig; + _saveCostConfig(); + + // 重新计算当日成本 + todayStats.value = todayStats.value.copyWith( + estimatedCost: _calculateEstimatedCost(todayStats.value), + ); + _saveTodayStats(); + + Logger.info('更新成本配置完成'); + } + + /// 更新预警阈值 + void updateThresholds({ + double? dailyThreshold, + double? monthlyThreshold, + }) { + if (dailyThreshold != null) { + dailyCostThreshold.value = dailyThreshold; + } + if (monthlyThreshold != null) { + monthlyCostThreshold.value = monthlyThreshold; + } + + // 保存阈值配置 + _storage.write('daily_cost_threshold', dailyCostThreshold.value); + _storage.write('monthly_cost_threshold', monthlyCostThreshold.value); + + Logger.info('更新预警阈值完成'); + } + + /// 获取月度统计汇总 + Future getMonthlyStats(int year, int month) async { + try { + final startDate = DateTime(year, month, 1); + final endDate = DateTime(year, month + 1, 0); // 月末 + + final dailyStats = await getHistoryStats( + startDate: startDate, + endDate: endDate, + ); + + if (dailyStats.isEmpty) { + return TranslationUsageStats.empty(getCurrentUserId()); + } + + // 汇总月度数据 + int totalTranslationCalls = 0; + int totalAsrCalls = 0; + int totalTtsCalls = 0; + int totalAiSummaryCalls = 0; + int totalSourceChars = 0; + int totalTargetChars = 0; + int totalRecordingDuration = 0; + int totalTtsPlayDuration = 0; + int totalSessions = 0; + double totalCost = 0.0; + + final Map combinedLanguagePairUsage = {}; + final Map combinedModeUsage = {}; + + for (final stats in dailyStats) { + totalTranslationCalls += stats.translationApiCalls; + totalAsrCalls += stats.asrApiCalls; + totalTtsCalls += stats.ttsApiCalls; + totalAiSummaryCalls += stats.aiSummaryApiCalls; + totalSourceChars += stats.sourceCharacterCount; + totalTargetChars += stats.targetCharacterCount; + totalRecordingDuration += stats.audioRecordingDuration; + totalTtsPlayDuration += stats.ttsPlaybackDuration; + totalSessions += stats.sessionCount; + totalCost += stats.estimatedCost; + + // 合并语言对使用统计 + stats.languagePairUsage.forEach((key, value) { + combinedLanguagePairUsage[key] = (combinedLanguagePairUsage[key] ?? 0) + value; + }); + + // 合并模式使用统计 + stats.modeUsage.forEach((key, value) { + combinedModeUsage[key] = (combinedModeUsage[key] ?? 0) + value; + }); + } + + return TranslationUsageStats( + id: 'monthly_${year}_$month', + date: startDate, + userId: getCurrentUserId(), + timeGranularity: 'month', + translationApiCalls: totalTranslationCalls, + asrApiCalls: totalAsrCalls, + ttsApiCalls: totalTtsCalls, + aiSummaryApiCalls: totalAiSummaryCalls, + sourceTextCharacters: totalSourceChars, + effectiveAudioDuration: totalRecordingDuration, + ttsTextCharacters: totalTargetChars, + totalSessionDuration: totalTtsPlayDuration, + sessionCount: totalSessions, + estimatedCost: totalCost, + ); + } catch (e) { + Logger.error('获取月度统计失败: $e'); + return TranslationUsageStats.empty(getCurrentUserId()); + } + } +} diff --git a/lib/modules/settings/views/settings_view.dart b/lib/modules/settings/views/settings_view.dart index 2b4955f9f..b6fd93d6f 100644 --- a/lib/modules/settings/views/settings_view.dart +++ b/lib/modules/settings/views/settings_view.dart @@ -407,6 +407,29 @@ class SettingsView extends GetView { ? Colors.white.withOpacity(0.1) : Colors.grey[200]), + // 翻译统计 + _buildSimpleNavigationSetting( + title: '翻译统计', // translationStats + subtitle: '查看翻译使用量和成本统计', // translationStatsInfo + icon: Icons.analytics_outlined, + iconBgColor: isDarkMode + ? Colors.green[900]!.withOpacity(0.3) + : Colors.green[100]!, + iconColor: + isDarkMode ? Colors.green[300]! : Colors.green[600]!, + titleColor: isDarkMode ? Colors.white : null, + subtitleColor: isDarkMode ? Colors.white70 : null, + onTap: () { + Get.toNamed(Routes.translationStats); + }, + isDarkMode: isDarkMode, + ), + Divider( + height: 1, + color: isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.grey[200]), + // 用户反馈 _buildSimpleNavigationSetting( title: 'feedback'.tr, // 用户反馈 diff --git a/lib/modules/translation/bindings/translation_stats_binding.dart b/lib/modules/translation/bindings/translation_stats_binding.dart new file mode 100644 index 000000000..031cdc0ea --- /dev/null +++ b/lib/modules/translation/bindings/translation_stats_binding.dart @@ -0,0 +1,18 @@ +import 'package:get/get.dart'; +import '../controllers/translation_stats_controller.dart'; +import '../../../data/services/translation_usage_service.dart'; + +class TranslationStatsBinding implements Bindings { + @override + void dependencies() { + // 确保服务已注册 + if (!Get.isRegistered()) { + Get.put(TranslationUsageService(), permanent: true); + } + + // 注册控制器 + Get.lazyPut( + () => TranslationStatsController(), + ); + } +} diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index 7290b4fbb..8a84e2f2b 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -1,4 +1,3 @@ -import 'dart:ffi'; import 'dart:io'; import '../../../core/utils/permission_util.dart'; @@ -15,8 +14,9 @@ import '../../../data/services/volcano_translation_service.dart'; import '../../../data/services/tts_service.dart'; import '../../../data/services/language_manager.dart'; import '../../../core/utils/logger.dart'; -import '../../../data/models/events/voice_interaction_event.dart'; + import '../../../data/services/asr_service.dart'; +import '../../../data/services/translation_usage_service.dart'; import '../../../data/services/ble_manager.dart'; // 切换音频输出设备的枚举 @@ -40,6 +40,15 @@ class TranslationController extends GetxController { final bleManager = Get.find(); //音乐播放管理 final _musiceManager = Get.find(); + // 统计服务 + late final TranslationUsageService _usageService; + + // ASR统计相关 - 精确时长统计 + DateTime? _asrSessionStartTime; // ASR会话开始时间 + DateTime? _asrActiveStartTime; // ASR活跃开始时间(用户实际开始语音识别的时间) + Duration _asrActiveDuration = Duration.zero; // ASR累计活跃时长 + bool _isAsrActive = false; // ASR是否处于活跃状态 + // 音频输入源 bool _audioSourceType = false; //是否开启播放 @@ -122,11 +131,16 @@ class TranslationController extends GetxController { // 监听识别结果 if (activeSpeaker.value != 0) { isPlayback = false; - final recognitionStream = - await _asrService.startContinuousRecognition(_audioSourceType); + await _asrService.startContinuousRecognition(_audioSourceType); + + // 开始ASR活跃时长跟踪(面对面模式) + _startAsrActiveTracking(); lastActiveSpeaker.value = id; } else { + // 停止ASR活跃时长跟踪(面对面模式) + _stopAsrActiveTracking(); + if (fTFTranslationResult != '' && !isPlayback) { isPlayback = true; await _ttsService.setAudioOutputDevice(lastActiveSpeaker.value); @@ -172,6 +186,9 @@ class TranslationController extends GetxController { super.onInit(); //_setupEventListeners(); + // 初始化统计服务 + _initUsageService(); + // 加载上次保存的语言设置 _loadSavedLanguages(); @@ -198,6 +215,20 @@ class TranslationController extends GetxController { // 参考Agent模块的实现模式 } + // 初始化统计服务 + void _initUsageService() { + try { + // 确保统计服务已注册 + if (!Get.isRegistered()) { + Get.put(TranslationUsageService(), permanent: true); + } + _usageService = Get.find(); + Logger.info('翻译统计服务初始化完成'); + } catch (e) { + Logger.error('翻译统计服务初始化失败: $e'); + } + } + // 加载保存的语言设置 void _loadSavedLanguages() { final savedSourceLanguage = _storage.read(_sourceLanguageKey); @@ -416,6 +447,17 @@ class TranslationController extends GetxController { } currentSessionId = DateTime.now().millisecondsSinceEpoch.toString(); + + // 开始新的翻译会话统计 + try { + _usageService.startNewSession(); + _asrSessionStartTime = DateTime.now(); // 记录ASR会话开始时间 + _asrActiveDuration = Duration.zero; // 重置ASR活跃时长 + _isAsrActive = false; // 重置ASR活跃状态 + } catch (e) { + Logger.error('开始新会话统计失败: $e'); + } + // 开始连续语音识别 if (currentMode.value == "audioVideo") { bleManager.openDecoder(); @@ -439,8 +481,9 @@ class TranslationController extends GetxController { _asrService.stopContinuousRecognition(); } else { await _ttsService.setAudioOutputDevice(0); - final recognitionStream = - await _asrService.startContinuousRecognition(_audioSourceType); + await _asrService.startContinuousRecognition(_audioSourceType); + // 开始ASR活跃时长计时 + _startAsrActiveTracking(); } isRecognizing.value = true; if (isRecording.value && isRecognizing.value) { @@ -481,6 +524,9 @@ class TranslationController extends GetxController { restoreOriginalAudioState(); await _asrService.stopContinuousRecognition(); + // 停止ASR活跃时长计时 + _stopAsrActiveTracking(); + isRecognizing.value = false; // 如果当前有中间结果,将其作为最终结果处理 @@ -558,6 +604,8 @@ class TranslationController extends GetxController { Future handleFinalResult(String text) async { if (text.isEmpty || translationHistory.isEmpty) return; + // ASR识别到最终结果,表示用户完成了一次有效的语音输入 + // 更新当前项 if (translationHistory.last.isIntermediate) { translationHistory.last.sourceText = text; @@ -660,6 +708,33 @@ class TranslationController extends GetxController { // 【改进】只有在最终结果时才保存,避免中间态时的频繁保存 if (isFinal) { saveTranslationHistory(); + + // 记录翻译API调用统计 - 优化版(精确计费参数) + try { + final sourceLanguageName = _languageManager + .getChineseNameByAsrCode(detectedSourceLanguageCode) ?? + '未知'; + final targetLanguageName = _languageManager + .getChineseNameByAsrCode(detectedTargetLanguageCode) ?? + '未知'; + + // 使用精确的计费参数:只统计源文本字符数(翻译API的实际计费基础) + _usageService.recordTranslationApiCall( + sourceText: sourceText, // 只有源文本用于翻译API计费 + targetText: translationResult, // 目标文本用于TTS计费 + sourceLanguage: sourceLanguageName, + targetLanguage: targetLanguageName, + mode: currentMode.value, + ); + + Logger.info( + '翻译统计: 源文本${sourceText.length}字符, 模式${currentMode.value}, 语言对$sourceLanguageName→$targetLanguageName'); + + // 记录ASR统计(在翻译完成时统计ASR使用情况) + _recordAsrUsageStats(sourceLanguageName); + } catch (e) { + Logger.error('记录翻译统计失败: $e'); + } } // 更新后滚动到底部 @@ -705,9 +780,32 @@ class TranslationController extends GetxController { final voiceName = _languageManager .getTtsVoiceNameByAsrCode(detectedTargetLanguageCode) ?? 'en-US-AriaNeural'; - print("voiceName=$voiceName,text$text"); + Logger.info("voiceName=$voiceName,text=$text"); + + final startTime = DateTime.now(); await _ttsService.setVoice(voiceName); await _ttsService.speakOnce(text); + final endTime = DateTime.now(); + + // 记录TTS API调用统计 - 优化版(基于文本字符数计费) + try { + final durationSeconds = endTime.difference(startTime).inSeconds; + final languageName = _languageManager + .getChineseNameByAsrCode(detectedTargetLanguageCode) ?? + '未知'; + + // TTS的主要计费依据是文本字符数,播放时长仅用于分析 + _usageService.recordTtsApiCall( + text: text, // 文本字符数是TTS计费的主要依据 + durationSeconds: durationSeconds, // 播放时长用于用户体验分析 + language: languageName, + ); + + Logger.info( + 'TTS统计: 文本${text.length}字符, 播放${durationSeconds}秒, 语言$languageName'); + } catch (e) { + Logger.error('记录TTS统计失败: $e'); + } } catch (e) { Logger.error('播放翻译失败: ${e.toString()}'); } @@ -724,6 +822,8 @@ class TranslationController extends GetxController { if (isRecognizing.value) { await stopRecognition(); } + // 确保停止ASR活跃时长跟踪 + _stopAsrActiveTracking(); stopRecording(); _translationDebounceTimer?.cancel(); await _ttsService.stop(); @@ -1210,6 +1310,54 @@ class TranslationController extends GetxController { void scrollToBottom({bool animate = true}) { _scrollToBottom(animate: animate); } + + /// 开始ASR活跃时长跟踪 + void _startAsrActiveTracking() { + if (!_isAsrActive) { + _asrActiveStartTime = DateTime.now(); + _isAsrActive = true; + Logger.info('开始ASR活跃时长跟踪'); + } + } + + /// 停止ASR活跃时长跟踪 + void _stopAsrActiveTracking() { + if (_isAsrActive && _asrActiveStartTime != null) { + final activeDuration = DateTime.now().difference(_asrActiveStartTime!); + _asrActiveDuration += activeDuration; + _isAsrActive = false; + _asrActiveStartTime = null; + Logger.info('停止ASR活跃时长跟踪,本次活跃时长: ${activeDuration.inSeconds}秒'); + } + } + + /// 记录ASR使用统计 - 基于精确时长跟踪 + void _recordAsrUsageStats(String language) { + try { + if (_asrSessionStartTime == null) return; + + // 确保停止当前的活跃时长跟踪 + _stopAsrActiveTracking(); + + final now = DateTime.now(); + final totalDurationSeconds = now.difference(_asrSessionStartTime!).inSeconds; + final effectiveDurationSeconds = _asrActiveDuration.inSeconds; + + _usageService.recordAsrApiCall( + effectiveDurationSeconds: effectiveDurationSeconds, + totalDurationSeconds: totalDurationSeconds, + language: language, + mode: currentMode.value, + audioFormat: 'pcm', + sampleRate: 16000, + ); + + Logger.info( + 'ASR会话统计: 总时长${totalDurationSeconds}秒, 实际活跃时长${effectiveDurationSeconds}秒, 语言$language'); + } catch (e) { + Logger.error('记录ASR统计失败: $e'); + } + } } // 翻译项模型 diff --git a/lib/modules/translation/controllers/translation_cost_controller.dart b/lib/modules/translation/controllers/translation_cost_controller.dart new file mode 100644 index 000000000..75b7fccff --- /dev/null +++ b/lib/modules/translation/controllers/translation_cost_controller.dart @@ -0,0 +1,258 @@ +import 'package:get/get.dart'; +import 'package:flutter/material.dart'; +import '../../../data/services/translation_usage_service.dart'; +import '../../../core/utils/logger.dart'; + +/// 翻译成本配置控制器 +class TranslationCostController extends GetxController { + // 依赖注入 + final TranslationUsageService _usageService = Get.find(); + + // 成本配置 + final RxMap costConfig = {}.obs; + final RxDouble dailyCostThreshold = 10.0.obs; + final RxDouble monthlyCostThreshold = 300.0.obs; + + // 表单控制器 + final translationCostController = TextEditingController(); + final asrCostController = TextEditingController(); + final ttsCostController = TextEditingController(); + final aiSummaryCostController = TextEditingController(); + final dailyThresholdController = TextEditingController(); + final monthlyThresholdController = TextEditingController(); + + // 状态变量 + final isLoading = false.obs; + final isSaving = false.obs; + + @override + void onInit() { + super.onInit(); + _loadCurrentConfig(); + } + + @override + void onClose() { + // 释放控制器 + translationCostController.dispose(); + asrCostController.dispose(); + ttsCostController.dispose(); + aiSummaryCostController.dispose(); + dailyThresholdController.dispose(); + monthlyThresholdController.dispose(); + super.onClose(); + } + + /// 加载当前配置 + void _loadCurrentConfig() { + isLoading.value = true; + + try { + // 从服务获取当前配置 + costConfig.value = Map.from(_usageService.costConfig); + dailyCostThreshold.value = _usageService.dailyCostThreshold.value; + monthlyCostThreshold.value = _usageService.monthlyCostThreshold.value; + + // 更新表单控制器 + translationCostController.text = (costConfig['translation_per_1k_chars'] ?? 0.05).toString(); + asrCostController.text = (costConfig['asr_per_minute'] ?? 0.02).toString(); + ttsCostController.text = (costConfig['tts_per_1k_chars'] ?? 0.03).toString(); + aiSummaryCostController.text = (costConfig['ai_summary_per_request'] ?? 0.01).toString(); + dailyThresholdController.text = dailyCostThreshold.value.toString(); + monthlyThresholdController.text = monthlyCostThreshold.value.toString(); + + Logger.info('成本配置加载完成'); + } catch (e) { + Logger.error('加载成本配置失败: $e'); + _showErrorSnackbar('加载配置失败'); + } finally { + isLoading.value = false; + } + } + + /// 保存配置 + Future saveConfig() async { + if (isSaving.value) return; + + isSaving.value = true; + + try { + // 验证输入 + if (!_validateInputs()) { + return; + } + + // 更新成本配置 + final newCostConfig = { + 'translation_per_1k_chars': double.parse(translationCostController.text), + 'asr_per_minute': double.parse(asrCostController.text), + 'tts_per_1k_chars': double.parse(ttsCostController.text), + 'ai_summary_per_request': double.parse(aiSummaryCostController.text), + }; + + final newDailyThreshold = double.parse(dailyThresholdController.text); + final newMonthlyThreshold = double.parse(monthlyThresholdController.text); + + // 保存到服务 + _usageService.updateCostConfig(newCostConfig); + _usageService.updateThresholds( + dailyThreshold: newDailyThreshold, + monthlyThreshold: newMonthlyThreshold, + ); + + // 更新本地状态 + costConfig.value = newCostConfig; + dailyCostThreshold.value = newDailyThreshold; + monthlyCostThreshold.value = newMonthlyThreshold; + + _showSuccessSnackbar('配置保存成功'); + Logger.info('成本配置保存成功'); + + } catch (e) { + Logger.error('保存成本配置失败: $e'); + _showErrorSnackbar('保存配置失败'); + } finally { + isSaving.value = false; + } + } + + /// 重置为默认配置 + void resetToDefault() { + translationCostController.text = '0.05'; + asrCostController.text = '0.02'; + ttsCostController.text = '0.03'; + aiSummaryCostController.text = '0.01'; + dailyThresholdController.text = '10.0'; + monthlyThresholdController.text = '300.0'; + + _showInfoSnackbar('已重置为默认配置,请点击保存生效'); + } + + /// 验证输入 + bool _validateInputs() { + try { + final translationCost = double.parse(translationCostController.text); + final asrCost = double.parse(asrCostController.text); + final ttsCost = double.parse(ttsCostController.text); + final aiSummaryCost = double.parse(aiSummaryCostController.text); + final dailyThreshold = double.parse(dailyThresholdController.text); + final monthlyThreshold = double.parse(monthlyThresholdController.text); + + if (translationCost < 0 || asrCost < 0 || ttsCost < 0 || aiSummaryCost < 0) { + _showErrorSnackbar('成本不能为负数'); + return false; + } + + if (dailyThreshold < 0 || monthlyThreshold < 0) { + _showErrorSnackbar('预警阈值不能为负数'); + return false; + } + + if (dailyThreshold > monthlyThreshold) { + _showErrorSnackbar('日预警阈值不能大于月预警阈值'); + return false; + } + + return true; + } catch (e) { + _showErrorSnackbar('请输入有效的数字'); + return false; + } + } + + /// 计算预估月成本 + double calculateEstimatedMonthlyCost() { + try { + final dailyStats = _usageService.todayStats.value; + if (dailyStats.estimatedCost == 0) return 0.0; + + // 简单按30天计算 + return dailyStats.estimatedCost * 30; + } catch (e) { + Logger.error('计算预估月成本失败: $e'); + return 0.0; + } + } + + /// 获取成本节约建议 + List getCostSavingTips() { + final tips = []; + + try { + final dailyStats = _usageService.todayStats.value; + + // 基于使用情况给出建议 + if (dailyStats.translationApiCalls > 100) { + tips.add('考虑批量翻译以减少API调用次数'); + } + + if (dailyStats.ttsApiCalls > 50) { + tips.add('可以关闭自动语音播放以节省TTS成本'); + } + + if (dailyStats.aiSummaryApiCalls > 10) { + tips.add('AI摘要功能按需使用,避免频繁调用'); + } + + if (dailyStats.effectiveAudioDuration > 3600) { // 超过1小时 + tips.add('长时间录音会增加ASR成本,建议分段录音'); + } + + if (tips.isEmpty) { + tips.add('您的使用习惯很好,继续保持!'); + } + + } catch (e) { + Logger.error('获取成本节约建议失败: $e'); + tips.add('暂无建议'); + } + + return tips; + } + + /// 显示成功提示 + void _showSuccessSnackbar(String message) { + Get.snackbar( + '成功', + message, + snackPosition: SnackPosition.TOP, + backgroundColor: Get.theme.colorScheme.primary.withOpacity(0.1), + colorText: Get.theme.colorScheme.primary, + duration: const Duration(seconds: 3), + ); + } + + /// 显示错误提示 + void _showErrorSnackbar(String message) { + Get.snackbar( + '错误', + message, + snackPosition: SnackPosition.TOP, + backgroundColor: Get.theme.colorScheme.error.withOpacity(0.1), + colorText: Get.theme.colorScheme.error, + duration: const Duration(seconds: 3), + ); + } + + /// 显示信息提示 + void _showInfoSnackbar(String message) { + Get.snackbar( + '提示', + message, + snackPosition: SnackPosition.TOP, + backgroundColor: Get.theme.colorScheme.surface.withOpacity(0.9), + colorText: Get.theme.colorScheme.onSurface, + duration: const Duration(seconds: 3), + ); + } + + /// 格式化成本显示 + String formatCost(double cost) { + return '¥${cost.toStringAsFixed(2)}'; + } + + /// 格式化百分比显示 + String formatPercentage(double percentage) { + return '${percentage.toStringAsFixed(1)}%'; + } +} diff --git a/lib/modules/translation/controllers/translation_detail_controller.dart b/lib/modules/translation/controllers/translation_detail_controller.dart index a33bf50ac..12a586aab 100644 --- a/lib/modules/translation/controllers/translation_detail_controller.dart +++ b/lib/modules/translation/controllers/translation_detail_controller.dart @@ -6,11 +6,13 @@ import '../controllers/translation_history_controller.dart'; import 'package:get_storage/get_storage.dart'; import 'dart:async'; import '../../../core/utils/logger.dart'; +import '../../../data/services/translation_usage_service.dart'; class TranslationDetailController extends GetxController { // 依赖注入 final VolcanoAIService _aiService = Get.find(); final GetStorage _storage = GetStorage(); + late final TranslationUsageService _usageService; // 存储键前缀 static const String _summaryKeyPrefix = 'translation_summary_'; @@ -33,13 +35,23 @@ class TranslationDetailController extends GetxController { @override void onInit() { super.onInit(); - + + // 初始化统计服务 + try { + if (!Get.isRegistered()) { + Get.put(TranslationUsageService(), permanent: true); + } + _usageService = Get.find(); + } catch (e) { + Logger.error('初始化统计服务失败: $e'); + } + // 从路由参数中获取数据 if (Get.arguments != null) { currentItem = Get.arguments['item']; sessionItems = Get.arguments['sessionItems']; historyController = Get.arguments['historyController']; - + // 异步加载数据,避免阻塞主线程 _asyncInitialize(); } else { @@ -170,8 +182,15 @@ class TranslationDetailController extends GetxController { if (result.isNotEmpty) { summaryResult.value = result; saveSummary(result); + + // 记录AI摘要API调用统计 + try { + _usageService.recordAiSummaryApiCall(); + } catch (e) { + Logger.error('记录AI摘要统计失败: $e'); + } } - + return result; } catch (e) { final errorMessage = '生成摘要失败: ${e.toString()}'; diff --git a/lib/modules/translation/controllers/translation_stats_controller.dart b/lib/modules/translation/controllers/translation_stats_controller.dart new file mode 100644 index 000000000..1e103b372 --- /dev/null +++ b/lib/modules/translation/controllers/translation_stats_controller.dart @@ -0,0 +1,358 @@ +import 'package:get/get.dart'; +import 'package:flutter/material.dart'; +import '../../../data/services/translation_usage_service.dart'; +import '../../../data/models/translation_usage_stats.dart'; +import '../../../core/utils/logger.dart'; + +/// 翻译统计页面控制器 +class TranslationStatsController extends GetxController { + // 依赖注入 + final TranslationUsageService _usageService = + Get.find(); + + // 状态变量 + final isLoading = false.obs; + final selectedTabIndex = 0.obs; // 0=今日, 1=本周, 2=本月, 3=历史 + final selectedTimeRange = '今日'.obs; + + // 统计数据 + final Rx todayStats = + TranslationUsageStats.empty('').obs; + final Rx weekStats = + TranslationUsageStats.empty('').obs; + final Rx monthStats = + TranslationUsageStats.empty('').obs; + final RxList historyStats = + [].obs; + + // 实时统计数据 + RealtimeUsageStats get realtimeStats => _usageService.realtimeStats.value; + + // 成本配置 + final RxMap costConfig = {}.obs; + final RxDouble dailyCostThreshold = 10.0.obs; + final RxDouble monthlyCostThreshold = 300.0.obs; + + // 图表数据 + final RxList> chartData = >[].obs; + + @override + void onInit() { + super.onInit(); + _initializeData(); + } + + /// 初始化数据 + void _initializeData() { + // 监听使用服务的数据变化 + ever(_usageService.todayStats, (stats) { + todayStats.value = stats; + _updateChartData(); + }); + + ever(_usageService.costConfig, (config) { + costConfig.value = config; + }); + + ever(_usageService.dailyCostThreshold, (threshold) { + dailyCostThreshold.value = threshold; + }); + + ever(_usageService.monthlyCostThreshold, (threshold) { + monthlyCostThreshold.value = threshold; + }); + + // 初始加载数据 + loadAllStats(); + } + + /// 加载所有统计数据 + Future loadAllStats() async { + isLoading.value = true; + + try { + // 加载今日数据 + todayStats.value = _usageService.todayStats.value; + + // 加载本周数据 + await _loadWeekStats(); + + // 加载本月数据 + await _loadMonthStats(); + + // 加载历史数据(最近7天) + await _loadHistoryStats(); + + // 更新图表数据 + _updateChartData(); + + Logger.info('统计数据加载完成'); + } catch (e) { + Logger.error('加载统计数据失败: $e'); + Get.snackbar( + '加载失败', + '统计数据加载失败,请稍后重试', + snackPosition: SnackPosition.TOP, + backgroundColor: Get.theme.colorScheme.error.withOpacity(0.1), + colorText: Get.theme.colorScheme.error, + ); + } finally { + isLoading.value = false; + } + } + + /// 加载本周统计数据 + Future _loadWeekStats() async { + try { + final now = DateTime.now(); + final weekStart = now.subtract(Duration(days: now.weekday - 1)); + final weekEnd = weekStart.add(const Duration(days: 6)); + + final dailyStats = await _usageService.getHistoryStats( + startDate: weekStart, + endDate: weekEnd, + ); + + weekStats.value = _aggregateStats(dailyStats, 'week'); + } catch (e) { + Logger.error('加载本周统计失败: $e'); + } + } + + /// 加载本月统计数据 + Future _loadMonthStats() async { + try { + final now = DateTime.now(); + monthStats.value = + await _usageService.getMonthlyStats(now.year, now.month); + } catch (e) { + Logger.error('加载本月统计失败: $e'); + } + } + + /// 加载历史统计数据 + Future _loadHistoryStats() async { + try { + final endDate = DateTime.now(); + final startDate = endDate.subtract(const Duration(days: 7)); + + historyStats.value = await _usageService.getHistoryStats( + startDate: startDate, + endDate: endDate, + ); + + // 按日期排序 + historyStats.sort((a, b) => b.date.compareTo(a.date)); + } catch (e) { + Logger.error('加载历史统计失败: $e'); + } + } + + /// 聚合统计数据 + TranslationUsageStats _aggregateStats( + List statsList, String period) { + if (statsList.isEmpty) { + return TranslationUsageStats.empty(_usageService.getCurrentUserId()); + } + + int totalTranslationCalls = 0; + int totalAsrCalls = 0; + int totalTtsCalls = 0; + int totalAiSummaryCalls = 0; + int totalSourceChars = 0; + int totalTargetChars = 0; + int totalRecordingDuration = 0; + int totalTtsPlayDuration = 0; + int totalSessions = 0; + double totalCost = 0.0; + + final Map combinedLanguagePairUsage = {}; + final Map combinedModeUsage = {}; + + for (final stats in statsList) { + totalTranslationCalls += stats.translationApiCalls; + totalAsrCalls += stats.asrApiCalls; + totalTtsCalls += stats.ttsApiCalls; + totalAiSummaryCalls += stats.aiSummaryApiCalls; + totalSourceChars += stats.sourceTextCharacters; + totalTargetChars += stats.ttsTextCharacters; + totalRecordingDuration += stats.effectiveAudioDuration; + totalTtsPlayDuration += stats.totalSessionDuration; + totalSessions += stats.sessionCount; + totalCost += stats.estimatedCost; + + // 合并语言对使用统计(使用兼容性getter) + stats.languagePairUsage.forEach((key, value) { + combinedLanguagePairUsage[key] = + (combinedLanguagePairUsage[key] ?? 0) + value; + }); + + // 合并模式使用统计(使用兼容性getter) + stats.modeUsage.forEach((key, value) { + combinedModeUsage[key] = (combinedModeUsage[key] ?? 0) + value; + }); + } + + return TranslationUsageStats( + id: '${period}_${DateTime.now().millisecondsSinceEpoch}', + date: DateTime.now(), + userId: 'current_user', + timeGranularity: period, + translationApiCalls: totalTranslationCalls, + asrApiCalls: totalAsrCalls, + ttsApiCalls: totalTtsCalls, + aiSummaryApiCalls: totalAiSummaryCalls, + sourceTextCharacters: totalSourceChars, + ttsTextCharacters: totalTargetChars, + effectiveAudioDuration: totalRecordingDuration, + totalSessionDuration: totalTtsPlayDuration, + sessionCount: totalSessions, + estimatedCost: totalCost, + ); + } + + /// 更新图表数据 + void _updateChartData() { + final List> data = []; + + // 根据选中的时间范围生成图表数据 + switch (selectedTabIndex.value) { + case 0: // 今日 + data.add({ + 'label': '今日', + 'apiCalls': todayStats.value.totalApiCalls, + 'cost': todayStats.value.estimatedCost, + 'characters': todayStats.value.totalCharacterCount, + }); + break; + case 1: // 本周 + data.add({ + 'label': '本周', + 'apiCalls': weekStats.value.totalApiCalls, + 'cost': weekStats.value.estimatedCost, + 'characters': weekStats.value.totalCharacterCount, + }); + break; + case 2: // 本月 + data.add({ + 'label': '本月', + 'apiCalls': monthStats.value.totalApiCalls, + 'cost': monthStats.value.estimatedCost, + 'characters': monthStats.value.totalCharacterCount, + }); + break; + case 3: // 历史 + for (final stats in historyStats.take(7)) { + data.add({ + 'label': '${stats.date.month}/${stats.date.day}', + 'apiCalls': stats.totalApiCalls, + 'cost': stats.estimatedCost, + 'characters': stats.totalCharacterCount, + }); + } + break; + } + + chartData.value = data; + } + + /// 切换时间范围 + void switchTimeRange(int index) { + selectedTabIndex.value = index; + + switch (index) { + case 0: + selectedTimeRange.value = '今日'; + break; + case 1: + selectedTimeRange.value = '本周'; + break; + case 2: + selectedTimeRange.value = '本月'; + break; + case 3: + selectedTimeRange.value = '历史'; + break; + } + + _updateChartData(); + } + + /// 获取当前显示的统计数据 + TranslationUsageStats get currentStats { + switch (selectedTabIndex.value) { + case 0: + return todayStats.value; + case 1: + return weekStats.value; + case 2: + return monthStats.value; + default: + return todayStats.value; + } + } + + /// 刷新数据 + Future refreshData() async { + await loadAllStats(); + } + + /// 清理过期数据 + void cleanupOldData() { + _usageService.cleanupOldData(); + Get.snackbar( + '清理完成', + '已清理90天前的过期数据', + snackPosition: SnackPosition.TOP, + backgroundColor: Get.theme.colorScheme.primary.withOpacity(0.1), + colorText: Get.theme.colorScheme.primary, + ); + } + + /// 导出统计数据 + Future exportStats() async { + try { + // TODO: 实现数据导出功能 + Get.snackbar( + '导出功能', + '数据导出功能正在开发中', + snackPosition: SnackPosition.TOP, + backgroundColor: Get.theme.colorScheme.primary.withOpacity(0.1), + colorText: Get.theme.colorScheme.primary, + ); + } catch (e) { + Logger.error('导出统计数据失败: $e'); + } + } + + /// 格式化时长显示 + String formatDuration(int seconds) { + if (seconds < 60) { + return '${seconds}秒'; + } else if (seconds < 3600) { + final minutes = seconds ~/ 60; + final remainingSeconds = seconds % 60; + return '${minutes}分${remainingSeconds}秒'; + } else { + final hours = seconds ~/ 3600; + final minutes = (seconds % 3600) ~/ 60; + return '${hours}小时${minutes}分'; + } + } + + /// 格式化成本显示 + String formatCost(double cost) { + return '¥${cost.toStringAsFixed(2)}'; + } + + /// 格式化数字显示 + String formatNumber(int number) { + if (number < 1000) { + return number.toString(); + } else if (number < 1000000) { + return '${(number / 1000).toStringAsFixed(1)}K'; + } else { + return '${(number / 1000000).toStringAsFixed(1)}M'; + } + } +} diff --git a/lib/modules/translation/views/translation_stats_view.dart b/lib/modules/translation/views/translation_stats_view.dart new file mode 100644 index 000000000..22773ddcb --- /dev/null +++ b/lib/modules/translation/views/translation_stats_view.dart @@ -0,0 +1,757 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import '../controllers/translation_stats_controller.dart'; +import '../../../core/theme/app_colors.dart'; + +/// 翻译统计页面 +class TranslationStatsView extends GetView { + const TranslationStatsView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.grey[50], + appBar: _buildAppBar(isDarkMode), + body: Obx(() => controller.isLoading.value + ? _buildLoadingWidget() + : _buildBody(isDarkMode)), + ); + } + + /// 构建应用栏 + PreferredSizeWidget _buildAppBar(bool isDarkMode) { + return AppBar( + backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.white, + elevation: 0.5, + title: Text( + '翻译统计', // translationStats + style: TextStyle( + fontSize: 17.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + centerTitle: true, + actions: [ + // 刷新按钮 + IconButton( + icon: Icon( + Icons.refresh, + color: isDarkMode ? Colors.white : Colors.black, + ), + onPressed: controller.refreshData, + ), + // 更多操作菜单 + PopupMenuButton( + icon: Icon( + Icons.more_vert, + color: isDarkMode ? Colors.white : Colors.black, + ), + onSelected: (value) { + switch (value) { + case 'cleanup': + controller.cleanupOldData(); + break; + case 'export': + controller.exportStats(); + break; + } + }, + itemBuilder: (context) => [ + const PopupMenuItem( + value: 'cleanup', + child: Text('清理过期数据'), + ), + const PopupMenuItem( + value: 'export', + child: Text('导出数据'), + ), + ], + ), + ], + ); + } + + /// 构建加载组件 + Widget _buildLoadingWidget() { + return const Center( + child: CircularProgressIndicator(), + ); + } + + /// 构建主体内容 + Widget _buildBody(bool isDarkMode) { + return Column( + children: [ + // 时间范围选择器 + _buildTimeRangeSelector(isDarkMode), + // 统计内容 + Expanded( + child: SingleChildScrollView( + padding: EdgeInsets.all(16.w), + child: Column( + children: [ + // 实时统计卡片 + if (controller.selectedTabIndex.value == 0) + _buildRealtimeStatsCard(isDarkMode), + SizedBox(height: 16.h), + // 概览统计卡片 + _buildOverviewCard(isDarkMode), + SizedBox(height: 16.h), + // API调用统计 + _buildApiCallsCard(isDarkMode), + SizedBox(height: 16.h), + // 数据量统计 + _buildDataVolumeCard(isDarkMode), + SizedBox(height: 16.h), + // 成本统计 + _buildCostCard(isDarkMode), + SizedBox(height: 16.h), + // 使用行为统计 + _buildUsageBehaviorCard(isDarkMode), + ], + ), + ), + ), + ], + ); + } + + /// 构建时间范围选择器 + Widget _buildTimeRangeSelector(bool isDarkMode) { + return Container( + margin: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? AppColors.cardBackground : Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Obx(() => Row( + children: [ + _buildTimeRangeTab('今日', 0, isDarkMode), + _buildTimeRangeTab('本周', 1, isDarkMode), + _buildTimeRangeTab('本月', 2, isDarkMode), + _buildTimeRangeTab('历史', 3, isDarkMode), + ], + )), + ); + } + + /// 构建时间范围标签 + Widget _buildTimeRangeTab(String title, int index, bool isDarkMode) { + final isSelected = controller.selectedTabIndex.value == index; + + return Expanded( + child: GestureDetector( + onTap: () => controller.switchTimeRange(index), + child: Container( + padding: EdgeInsets.symmetric(vertical: 12.h), + decoration: BoxDecoration( + color: isSelected + ? (isDarkMode ? AppColors.primary : AppColors.primary) + : Colors.transparent, + borderRadius: BorderRadius.circular(12.r), + ), + child: Text( + title, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14.sp, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, + color: isSelected + ? Colors.white + : (isDarkMode ? Colors.white70 : Colors.black54), + ), + ), + ), + ), + ); + } + + /// 构建实时统计卡片 + Widget _buildRealtimeStatsCard(bool isDarkMode) { + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? AppColors.cardBackground : 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: [ + Icon( + Icons.access_time, + color: AppColors.primary, + size: 20.sp, + ), + SizedBox(width: 8.w), + Text( + '当前会话统计', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ], + ), + SizedBox(height: 16.h), + Obx(() => Row( + children: [ + Expanded( + child: _buildStatItem( + '翻译次数', + controller.realtimeStats.translationCount.toString(), + Icons.translate, + isDarkMode, + ), + ), + Expanded( + child: _buildStatItem( + '会话时长', + '${controller.realtimeStats.sessionDurationMinutes}分钟', + Icons.timer, + isDarkMode, + ), + ), + ], + )), + SizedBox(height: 12.h), + Obx(() => Row( + children: [ + Expanded( + child: _buildStatItem( + '字符数', + controller.formatNumber( + controller.realtimeStats.sourceCharacters + + controller.realtimeStats.targetCharacters, + ), + Icons.text_fields, + isDarkMode, + ), + ), + Expanded( + child: _buildStatItem( + '录音时长', + controller.formatDuration(controller.realtimeStats.recordingDuration), + Icons.mic, + isDarkMode, + ), + ), + ], + )), + ], + ), + ); + } + + /// 构建概览卡片 + Widget _buildOverviewCard(bool isDarkMode) { + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? AppColors.cardBackground : 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: [ + Icon( + Icons.dashboard, + color: AppColors.primary, + size: 20.sp, + ), + SizedBox(width: 8.w), + Obx(() => Text( + '${controller.selectedTimeRange.value}概览', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + )), + ], + ), + SizedBox(height: 16.h), + Obx(() => Row( + children: [ + Expanded( + child: _buildStatItem( + '总调用次数', + controller.formatNumber(controller.currentStats.totalApiCalls), + Icons.api, + isDarkMode, + ), + ), + Expanded( + child: _buildStatItem( + '预估成本', + controller.formatCost(controller.currentStats.estimatedCost), + Icons.attach_money, + isDarkMode, + ), + ), + ], + )), + SizedBox(height: 12.h), + Obx(() => Row( + children: [ + Expanded( + child: _buildStatItem( + '会话数量', + controller.currentStats.sessionCount.toString(), + Icons.chat, + isDarkMode, + ), + ), + Expanded( + child: _buildStatItem( + '总字符数', + controller.formatNumber(controller.currentStats.totalCharacterCount), + Icons.text_snippet, + isDarkMode, + ), + ), + ], + )), + ], + ), + ); + } + + /// 构建统计项 + Widget _buildStatItem(String label, String value, IconData icon, bool isDarkMode) { + return Column( + children: [ + Icon( + icon, + color: AppColors.primary, + size: 24.sp, + ), + SizedBox(height: 8.h), + Text( + value, + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.bold, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + SizedBox(height: 4.h), + Text( + label, + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.black54, + ), + ), + ], + ); + } + + /// 构建API调用统计卡片 + Widget _buildApiCallsCard(bool isDarkMode) { + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? AppColors.cardBackground : 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: [ + Icon( + Icons.api, + color: AppColors.primary, + size: 20.sp, + ), + SizedBox(width: 8.w), + Text( + 'API调用统计', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ], + ), + SizedBox(height: 16.h), + Obx(() => Column( + children: [ + _buildApiCallRow('翻译API', controller.currentStats.translationApiCalls, Icons.translate, isDarkMode), + SizedBox(height: 12.h), + _buildApiCallRow('语音识别', controller.currentStats.asrApiCalls, Icons.mic, isDarkMode), + SizedBox(height: 12.h), + _buildApiCallRow('语音合成', controller.currentStats.ttsApiCalls, Icons.volume_up, isDarkMode), + SizedBox(height: 12.h), + _buildApiCallRow('AI摘要', controller.currentStats.aiSummaryApiCalls, Icons.summarize, isDarkMode), + ], + )), + ], + ), + ); + } + + /// 构建API调用行 + Widget _buildApiCallRow(String label, int count, IconData icon, bool isDarkMode) { + return Row( + children: [ + Icon( + icon, + color: AppColors.primary, + size: 18.sp, + ), + SizedBox(width: 12.w), + Expanded( + child: Text( + label, + style: TextStyle( + fontSize: 14.sp, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ), + Text( + controller.formatNumber(count), + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ], + ); + } + + /// 构建数据量统计卡片 + Widget _buildDataVolumeCard(bool isDarkMode) { + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? AppColors.cardBackground : 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: [ + Icon( + Icons.data_usage, + color: AppColors.primary, + size: 20.sp, + ), + SizedBox(width: 8.w), + Text( + '数据量统计', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ], + ), + SizedBox(height: 16.h), + Obx(() => Column( + children: [ + Row( + children: [ + Expanded( + child: _buildStatItem( + '源文本字符', + controller.formatNumber(controller.currentStats.sourceTextCharacters), + Icons.text_fields, + isDarkMode, + ), + ), + Expanded( + child: _buildStatItem( + 'TTS文本字符', + controller.formatNumber(controller.currentStats.ttsTextCharacters), + Icons.translate, + isDarkMode, + ), + ), + ], + ), + SizedBox(height: 16.h), + Row( + children: [ + Expanded( + child: _buildStatItem( + '有效音频时长', + controller.formatDuration(controller.currentStats.effectiveAudioDuration), + Icons.mic, + isDarkMode, + ), + ), + Expanded( + child: _buildStatItem( + '总会话时长', + controller.formatDuration(controller.currentStats.totalSessionDuration), + Icons.volume_up, + isDarkMode, + ), + ), + ], + ), + ], + )), + ], + ), + ); + } + + /// 构建成本统计卡片 + Widget _buildCostCard(bool isDarkMode) { + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? AppColors.cardBackground : 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: [ + Icon( + Icons.attach_money, + color: AppColors.primary, + size: 20.sp, + ), + SizedBox(width: 8.w), + Text( + '成本统计', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ], + ), + SizedBox(height: 16.h), + Obx(() => Column( + children: [ + // 当前成本 + Container( + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + color: AppColors.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(8.r), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${controller.selectedTimeRange.value}预估成本', + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w500, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + Text( + controller.formatCost(controller.currentStats.estimatedCost), + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.bold, + color: AppColors.primary, + ), + ), + ], + ), + ), + SizedBox(height: 12.h), + // 预警阈值 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '日成本预警阈值', + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.black54, + ), + ), + Text( + controller.formatCost(controller.dailyCostThreshold.value), + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.black54, + ), + ), + ], + ), + SizedBox(height: 4.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '月成本预警阈值', + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.black54, + ), + ), + Text( + controller.formatCost(controller.monthlyCostThreshold.value), + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.black54, + ), + ), + ], + ), + ], + )), + ], + ), + ); + } + + /// 构建使用行为统计卡片 + Widget _buildUsageBehaviorCard(bool isDarkMode) { + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? AppColors.cardBackground : 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: [ + Icon( + Icons.analytics, + color: AppColors.primary, + size: 20.sp, + ), + SizedBox(width: 8.w), + Text( + '使用行为统计', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ], + ), + SizedBox(height: 16.h), + Obx(() => Column( + children: [ + // 最常用语言对 + _buildUsageBehaviorRow( + '最常用语言对', + controller.currentStats.mostUsedLanguagePair, + Icons.language, + isDarkMode, + ), + SizedBox(height: 12.h), + // 最常用翻译模式 + _buildUsageBehaviorRow( + '最常用模式', + controller.currentStats.mostUsedMode, + Icons.settings, + isDarkMode, + ), + ], + )), + ], + ), + ); + } + + /// 构建使用行为行 + Widget _buildUsageBehaviorRow(String label, String value, IconData icon, bool isDarkMode) { + return Row( + children: [ + Icon( + icon, + color: AppColors.primary, + size: 18.sp, + ), + SizedBox(width: 12.w), + Expanded( + child: Text( + label, + style: TextStyle( + fontSize: 14.sp, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ), + Text( + value, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ], + ); + } +} diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart index 10d791d6c..6da9b95fe 100644 --- a/lib/routes/app_pages.dart +++ b/lib/routes/app_pages.dart @@ -28,6 +28,8 @@ import '../modules/timbre/views/timbre_view.dart'; import '../modules/translation/views/translation_view.dart'; import '../modules/translation/bindings/translation_binding.dart'; import '../modules/translation/views/translation_history_view.dart'; +import '../modules/translation/views/translation_stats_view.dart'; +import '../modules/translation/bindings/translation_stats_binding.dart'; import '../modules/translation/bindings/translation_list_binding.dart'; import '../modules/translation/views/translation_list_view.dart'; import '../modules/translation/bindings/translation_history_binding.dart'; @@ -148,6 +150,13 @@ abstract class AppPages { transition: Transition.rightToLeft, transitionDuration: Duration(milliseconds: 250), ), + GetPage( + name: Routes.translationStats, + page: () => const TranslationStatsView(), + binding: TranslationStatsBinding(), + transition: Transition.rightToLeft, + transitionDuration: Duration(milliseconds: 250), + ), GetPage( name: Routes.translationList, page: () => const TranslationListView(), diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index 3559da01e..1b677f334 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -28,6 +28,7 @@ abstract class Routes { // 语言功能 static const translation = '/translation'; // 实时翻译 static const translationHistory = '/translation/history'; // 翻译历史 + static const translationStats = '/translation/stats'; // 翻译统计 static const speechDemo = '/speech_demo'; // 语音演示 static const translationList = '/translation/List'; // 翻译历史 static const ftftranslation = '/ftftranslation'; // 面对面翻译 From 597a7b2e768a332067311a9b8cd29543a9a92557 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Sat, 19 Jul 2025 16:28:59 +0800 Subject: [PATCH 04/12] =?UTF-8?q?fix(=E8=93=9D=E7=89=99=E9=85=8D=E5=AF=B9)?= =?UTF-8?q?:=20=E4=BC=98=E5=8C=96BLE=E8=BF=9E=E6=8E=A5=E8=B6=85=E6=97=B6?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E5=92=8C=E8=B6=85=E6=97=B6=E6=97=B6=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将BLE连接超时时间从30秒缩短至20秒 更新中英文超时提示,建议用户重启耳机后重试 --- lib/core/translations/language/en_us.dart | 3 ++- lib/core/translations/language/zh_cn.dart | 2 +- lib/modules/pairing/controllers/pairing_controller.dart | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/core/translations/language/en_us.dart b/lib/core/translations/language/en_us.dart index 5af4469ef..572e595af 100644 --- a/lib/core/translations/language/en_us.dart +++ b/lib/core/translations/language/en_us.dart @@ -805,7 +805,8 @@ const Map enUS = { 'invalidDeviceAddress': 'Invalid device address', 'bleDeviceAssociationFailed': 'BLE device association failed', 'deviceAssociationFailedRetry': 'Device association failed, please retry', - 'bleConnectionTimeoutRetry': 'BLE connection timeout, please retry', + 'bleConnectionTimeoutRetry': + 'BLE connection timeout. Please restart the earphone and try again', 'blePairingStartupFailed': 'BLE pairing startup failed', 'bleConnectionFailed': 'BLE connection failed', 'deviceAuthorizationFailedCheckCode': diff --git a/lib/core/translations/language/zh_cn.dart b/lib/core/translations/language/zh_cn.dart index ebf87b6d8..4c1e73d89 100644 --- a/lib/core/translations/language/zh_cn.dart +++ b/lib/core/translations/language/zh_cn.dart @@ -741,7 +741,7 @@ const Map zhCN = { 'invalidDeviceAddress': '设备地址无效', 'bleDeviceAssociationFailed': 'BLE设备关联失败', 'deviceAssociationFailedRetry': '设备关联失败,请重试', - 'bleConnectionTimeoutRetry': 'BLE连接超时,请重试', + 'bleConnectionTimeoutRetry': 'BLE连接超时,请重启耳机后再试', 'blePairingStartupFailed': 'BLE配对启动失败', 'bleConnectionFailed': 'BLE连接失败', 'deviceAuthorizationFailedCheckCode': '设备授权失败,请检查授权码', diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index ec5902c1b..fff2c7bdf 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -552,7 +552,7 @@ class PairingController extends GetxController { try { // 设置超时定时器 _bleTimeoutTimer?.cancel(); - _bleTimeoutTimer = Timer(const Duration(seconds: 30), () { + _bleTimeoutTimer = Timer(const Duration(seconds: 20), () { if (_currentState.value == PairingState.waitingBle) { _setError(ErrorType.bleTimeout, 'bleConnectionTimeoutRetry'.tr); //BLE连接超时,请重试 From 1061812bda3d9a09fea1fe79be5e4aed5d874a00 Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Sun, 20 Jul 2025 00:58:14 +0800 Subject: [PATCH 05/12] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=8F=91=E9=80=81ble?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=E6=B2=A1=E6=9C=89=E5=9B=9E=E5=A4=8D=E3=80=82?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=E5=90=8E=E7=BB=AD=E7=9A=84=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E5=8F=91=E9=80=81=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yunqiinnovation/ble_service/BleService.kt | 54 ++++++++++++------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt index af78e6d49..999dd7efc 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt @@ -106,6 +106,15 @@ object BleService { // 初始化状态 private var isInitialized = false + private val replyTimeoutHandler = Handler(Looper.getMainLooper()) + private val replyTimeoutRunnable = Runnable { + if (!isReply) { + Log.w(TAG, "命令响应超时,自动重置 isReply 为 true") + isReply = true + processNextCommand() + } + } + /** * 初始化BLE服务 * @param appContext 应用上下文 @@ -471,6 +480,8 @@ object BleService { } catch (e: Exception) { Log.e(TAG, "关闭GATT连接异常: ${e.message}", e) } + isReply = true; + commandQueue.clear() bluetoothGatt = null notifyChar = null writeChar = null @@ -520,6 +531,7 @@ object BleService { newState == BluetoothProfile.STATE_DISCONNECTED -> { updateConnectionState(BleConst.STATE_DISCONNECTED) disconnectGatt() + // otaManager?.onBtDeviceConnection(g.device, StateCode.CONNECTION_CONNECTING) // g.close() // otaManager?.release(); @@ -752,19 +764,10 @@ object BleService { } }" ) - isReply = true; - cmdReplyType = cmdType; - if (!commandQueue.isEmpty()) { - val nextCmd = commandQueue.poll() - writeChar!!.value = nextCmd - var isSeccess = bluetoothGatt?.writeCharacteristic(writeChar) - if (isSeccess == true) { - Log.d( - TAG, - "收到设备响应:" - ) - } - } + isReply = true + cmdReplyType = cmdType + replyTimeoutHandler.removeCallbacks(replyTimeoutRunnable) // 移除超时回调 + processNextCommand() // 根据命令类型处理不同的响应 when (cmdType) { BleConst.CMD_GET_VERSION -> { @@ -1385,13 +1388,16 @@ object BleService { // 加入队列并触发发送 if (isReply) { - var isSeccess = bluetoothGatt?.writeCharacteristic(writeChar) - if (isSeccess == true) { - isReply = false; - cmdType; + val isSuccess = bluetoothGatt?.writeCharacteristic(writeChar) + if (isSuccess == true) { + isReply = false + replyTimeoutHandler.postDelayed(replyTimeoutRunnable, 1000) // 设置1秒超时 return true } else + { + commandQueue.poll() return false + } } else { commandQueue.offer(cmdPacket) } @@ -1403,6 +1409,19 @@ object BleService { } } + private fun processNextCommand() { + if (!commandQueue.isEmpty()) { + val nextCmd = commandQueue.poll() + writeChar!!.value = nextCmd + val isSuccess = bluetoothGatt?.writeCharacteristic(writeChar) + if (isSuccess == true) { + isReply = false + replyTimeoutHandler.postDelayed(replyTimeoutRunnable, 1000) // 设置1秒超时 + Log.d(TAG, "发送队列中的下一个命令") + } + } + } + /** * 计算CRC8/MAXIM校验值 */ @@ -1588,4 +1607,3 @@ class RecordingFile(private val context: Context) { } } } - \ No newline at end of file From 96b0f381b85e6412879579a519abeaf559e0d92c Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Sun, 20 Jul 2025 09:29:09 +0800 Subject: [PATCH 06/12] =?UTF-8?q?fix:=20=E6=9B=B4=E6=96=B0BLE=E8=B6=85?= =?UTF-8?q?=E6=97=B6=E6=8F=90=E7=A4=BA=E5=92=8C=E8=B0=83=E6=95=B4=E8=A7=86?= =?UTF-8?q?=E5=9B=BE=E9=AB=98=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修改BLE连接超时提示信息,建议用户重启耳机后重试 调整agent视图容器高度从50.h改为52.h --- lib/modules/agent/views/agent_view.dart | 2 +- lib/modules/pairing/controllers/pairing_controller.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index 7e048d2fe..3703ec3f4 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -530,7 +530,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { controller.isListening.value; return Container( - height: 50.h, + height: 52.h, padding: EdgeInsets.symmetric( horizontal: 16.w), decoration: BoxDecoration( diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index fff2c7bdf..280f8cf58 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -555,7 +555,7 @@ class PairingController extends GetxController { _bleTimeoutTimer = Timer(const Duration(seconds: 20), () { if (_currentState.value == PairingState.waitingBle) { _setError(ErrorType.bleTimeout, - 'bleConnectionTimeoutRetry'.tr); //BLE连接超时,请重试 + 'bleConnectionTimeoutRetry'.tr); //BLE连接超时,请重启耳机后再试 } }); From a8ed559d9a0d1d1cff7d01eb164a68345fd1d07c Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Sun, 20 Jul 2025 12:13:18 +0800 Subject: [PATCH 07/12] =?UTF-8?q?feat(profile):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E9=80=80=E5=87=BA=E6=97=B6=E5=BC=BA=E5=88=B6=E5=9B=9E=E6=BB=9A?= =?UTF-8?q?=E6=9C=AA=E4=BF=9D=E5=AD=98=E6=9B=B4=E6=94=B9=E7=9A=84=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在用户退出编辑页面时自动检查并回滚未保存的更改,防止数据丢失。修改了编辑资料和会议资料视图,添加了WillPopScope处理返回操作。 --- .../controllers/profile_controller.dart | 9 +++ .../profile/views/profile_meeting_view.dart | 68 +++++++++++-------- lib/modules/profile/views/profile_view.dart | 31 ++++++--- 3 files changed, 68 insertions(+), 40 deletions(-) diff --git a/lib/modules/profile/controllers/profile_controller.dart b/lib/modules/profile/controllers/profile_controller.dart index e679fecb7..a26ec587e 100644 --- a/lib/modules/profile/controllers/profile_controller.dart +++ b/lib/modules/profile/controllers/profile_controller.dart @@ -192,6 +192,15 @@ class ProfileController extends GetxController { // } // } + // 强制回滚未保存的更改(在页面退出时调用) + void forceRollbackIfUnsaved() { + if (hasEdits.value) { + Logger().d('检测到未保存的编辑,执行强制回滚'); + _rollbackToOriginalState(); // 复用现有的回滚方法 + } + isEditingNickname.value = false; + } + // 更新用户信息到后端 Future _syncWithBackend() async { try { diff --git a/lib/modules/profile/views/profile_meeting_view.dart b/lib/modules/profile/views/profile_meeting_view.dart index ccf7d29a1..dcda97711 100644 --- a/lib/modules/profile/views/profile_meeting_view.dart +++ b/lib/modules/profile/views/profile_meeting_view.dart @@ -34,7 +34,10 @@ class ProfileMeetingView extends GetView { color: isDarkMode ? Colors.white : Colors.black87, size: 20.w, ), - onPressed: () => Get.back(), + onPressed: () { + controller.forceRollbackIfUnsaved(); + Get.back(); + }, ), title: Text( '账号和资料', @@ -92,35 +95,40 @@ class ProfileMeetingView extends GetView { } Widget _buildBody(bool isDarkMode) { - return SafeArea( - child: ListView( - padding: EdgeInsets.symmetric(horizontal: 16.w), - children: [ - SizedBox(height: 4.h), - _buildAvatarSection(isDarkMode), - SizedBox(height: 16.h), - _buildNicknameItem(isDarkMode), - Divider( - height: 0.5.h, - color: isDarkMode ? Colors.grey[800] : Colors.grey[200]), - _buildLoginMethodItem(isDarkMode), - Divider( - height: 0.5.h, - color: isDarkMode ? Colors.grey[800] : Colors.grey[200]), - _buildManageLoginItem(isDarkMode), - Divider( - height: 0.5.h, - color: isDarkMode ? Colors.grey[800] : Colors.grey[200]), - SizedBox(height: 32.h), - _buildLogoutButton(isDarkMode), - SizedBox(height: 16.h), - _buildDeleteAccountButton(isDarkMode), - SizedBox(height: 8.h), - _buildWarningText(), - SizedBox(height: 32.h), - ], - ), - ); + return WillPopScope( + onWillPop: () async { + controller.forceRollbackIfUnsaved(); + return true; + }, + child: SafeArea( + child: ListView( + padding: EdgeInsets.symmetric(horizontal: 16.w), + children: [ + SizedBox(height: 4.h), + _buildAvatarSection(isDarkMode), + SizedBox(height: 16.h), + _buildNicknameItem(isDarkMode), + Divider( + height: 0.5.h, + color: isDarkMode ? Colors.grey[800] : Colors.grey[200]), + _buildLoginMethodItem(isDarkMode), + Divider( + height: 0.5.h, + color: isDarkMode ? Colors.grey[800] : Colors.grey[200]), + _buildManageLoginItem(isDarkMode), + Divider( + height: 0.5.h, + color: isDarkMode ? Colors.grey[800] : Colors.grey[200]), + SizedBox(height: 32.h), + _buildLogoutButton(isDarkMode), + SizedBox(height: 16.h), + _buildDeleteAccountButton(isDarkMode), + SizedBox(height: 8.h), + _buildWarningText(), + SizedBox(height: 32.h), + ], + ), + )); } Widget _buildAvatarSection(bool isDarkMode) { diff --git a/lib/modules/profile/views/profile_view.dart b/lib/modules/profile/views/profile_view.dart index dd3f1dea3..6f18f6d9f 100644 --- a/lib/modules/profile/views/profile_view.dart +++ b/lib/modules/profile/views/profile_view.dart @@ -158,7 +158,10 @@ class EditProfileView extends GetView { size: 20.sp, color: isDarkMode ? Colors.white : Colors.black, ), - onPressed: () => Get.back(), + onPressed: () { + controller.forceRollbackIfUnsaved(); + Get.back(); + }, ), // title: Text( // 'editProfile'.tr, @@ -214,15 +217,23 @@ class EditProfileView extends GetView { )) ], ), - body: Padding( - padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), - child: - Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildEditProfileSection(context, isDarkMode, nameController), - SizedBox(height: 16.h), - _buildAccountSettings(isDarkMode), - SizedBox(height: 16.h), - ]))); + // ignore: deprecated_member_use + body: WillPopScope( + onWillPop: () async { + ProfileController.to.forceRollbackIfUnsaved(); + return true; + }, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildEditProfileSection( + context, isDarkMode, nameController), + SizedBox(height: 16.h), + _buildAccountSettings(isDarkMode), + SizedBox(height: 16.h), + ])))); } // 新增:构建编辑资料区域的方法 From 02e783d7708fb1a012ccd6a2bf93637c0def39c0 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Sun, 20 Jul 2025 14:12:42 +0800 Subject: [PATCH 08/12] =?UTF-8?q?feat(=E6=B6=88=E6=81=AF=E6=B0=94=E6=B3=A1?= =?UTF-8?q?):=20=E6=B7=BB=E5=8A=A0=E6=99=BA=E8=83=BD=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E6=88=B3=E6=A0=BC=E5=BC=8F=E5=8C=96=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在消息气泡组件中实现智能时间显示逻辑,根据时间差显示"刚刚"、"X分钟前"、"昨天"等友好格式 同时添加中英文时间相关翻译字段 --- lib/core/translations/language/en_us.dart | 12 ++++ lib/core/translations/language/zh_cn.dart | 5 ++ lib/modules/agent/views/message_bubble.dart | 77 +++++++++++++++++++-- 3 files changed, 90 insertions(+), 4 deletions(-) diff --git a/lib/core/translations/language/en_us.dart b/lib/core/translations/language/en_us.dart index 572e595af..afb5f6242 100644 --- a/lib/core/translations/language/en_us.dart +++ b/lib/core/translations/language/en_us.dart @@ -893,4 +893,16 @@ const Map enUS = { "乌尔都语": "Urdu", "波斯语": "Persian", "斯瓦希里语": "Swahili", + + // Time related + "justNow": "Just now", + "minutesAgo": "@minutes minutes ago", + "yesterday": "Yesterday", + // "monday": "Monday", + // "tuesday": "Tuesday", + // "wednesday": "Wednesday", + // "thursday": "Thursday", + // "friday": "Friday", + // "saturday": "Saturday", + // "sunday": "Sunday", }; diff --git a/lib/core/translations/language/zh_cn.dart b/lib/core/translations/language/zh_cn.dart index 4c1e73d89..23aab872a 100644 --- a/lib/core/translations/language/zh_cn.dart +++ b/lib/core/translations/language/zh_cn.dart @@ -824,4 +824,9 @@ const Map zhCN = { "乌尔都语": "乌尔都语", "波斯语": "波斯语", "斯瓦希里语": "斯瓦希里语", + + // 时间相关 + "justNow": "刚刚", + "minutesAgo": "@minutes分钟前", + "yesterday": "昨天", }; diff --git a/lib/modules/agent/views/message_bubble.dart b/lib/modules/agent/views/message_bubble.dart index 440dc1b0a..b8903cc5f 100644 --- a/lib/modules/agent/views/message_bubble.dart +++ b/lib/modules/agent/views/message_bubble.dart @@ -33,8 +33,6 @@ class MessageBubble extends GetView { @override Widget build(BuildContext context) { - final timeFormat = DateFormat('HH:mm'); - return Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( @@ -91,7 +89,7 @@ class MessageBubble extends GetView { children: [ // 时间戳(左对齐或右对齐) Text( - timeFormat.format(timestamp), + _formatTimestamp(timestamp), style: TextStyle( fontSize: 10, color: Theme.of(context).brightness == Brightness.dark @@ -105,7 +103,7 @@ class MessageBubble extends GetView { Padding( padding: const EdgeInsets.only(left: 8), child: Text( - "generatedByAi".tr,// 由AI生成 + "generatedByAi".tr, // 由AI生成 style: TextStyle( fontSize: 8, color: Colors.blue[600], @@ -418,6 +416,77 @@ class MessageBubble extends GetView { Widget _buildDot(int milliseconds) { return _AnimatedDot(delayMilliseconds: milliseconds); } + + /// 智能格式化时间戳 + String _formatTimestamp(DateTime messageTime) { + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final yesterday = today.subtract(const Duration(days: 1)); + final messageDate = + DateTime(messageTime.year, messageTime.month, messageTime.day); + + // 计算时间差 + final difference = now.difference(messageTime); + + // 1分钟内显示"刚刚" + if (difference.inMinutes < 1) { + return 'justNow'.tr; // 刚刚 + } + + // 1小时内显示"X分钟前" + if (difference.inHours < 1) { + return 'minutesAgo' + .trParams({'minutes': difference.inMinutes.toString()}); // X分钟前 + } + + // 今天的消息显示时间 + if (messageDate == today) { + return DateFormat('HH:mm').format(messageTime); + } + + // 昨天的消息显示"昨天 HH:mm" + if (messageDate == yesterday) { + return 'yesterday'.tr + + ' ' + + DateFormat('HH:mm').format(messageTime); // 昨天 HH:mm + } + + // 本周内的消息显示"星期X HH:mm" + if (difference.inDays < 7) { + final weekday = _getWeekdayName(messageTime.weekday); + return weekday + ' ' + DateFormat('HH:mm').format(messageTime); + } + + // 今年的消息显示"MM-dd HH:mm" + if (messageTime.year == now.year) { + return DateFormat('MM-dd HH:mm').format(messageTime); + } + + // 其他情况显示完整日期"yyyy-MM-dd HH:mm" + return DateFormat('yyyy-MM-dd HH:mm').format(messageTime); + } + + /// 获取星期名称 + String _getWeekdayName(int weekday) { + switch (weekday) { + case 1: + return 'monday'.tr; // 星期一 + case 2: + return 'tuesday'.tr; // 星期二 + case 3: + return 'wednesday'.tr; // 星期三 + case 4: + return 'thursday'.tr; // 星期四 + case 5: + return 'friday'.tr; // 星期五 + case 6: + return 'saturday'.tr; // 星期六 + case 7: + return 'sunday'.tr; // 星期日 + default: + return ''; + } + } } /// 动画点组件 From 0fda67a9c1852e03550d5b8484010733680e3f56 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Sun, 20 Jul 2025 14:47:20 +0800 Subject: [PATCH 09/12] =?UTF-8?q?feat(explore):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E7=BF=BB=E8=AF=91=E7=BB=9F=E8=AE=A1=E9=A1=B5=E9=9D=A2=E8=B7=B3?= =?UTF-8?q?=E8=BD=AC=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在探索控制器中添加跳转到翻译统计页面的方法 - 将设置页面的翻译统计入口移动到探索页面 - 修改探索页面图标并绑定跳转事件 --- .../controllers/explore_controller.dart | 5 ++ lib/modules/explore/views/explore_view.dart | 72 ++++++++++--------- lib/modules/settings/views/settings_view.dart | 23 +----- 3 files changed, 44 insertions(+), 56 deletions(-) diff --git a/lib/modules/explore/controllers/explore_controller.dart b/lib/modules/explore/controllers/explore_controller.dart index f09d1e1bd..e03ed7d1d 100644 --- a/lib/modules/explore/controllers/explore_controller.dart +++ b/lib/modules/explore/controllers/explore_controller.dart @@ -263,6 +263,11 @@ class ExploreController extends GetxController { // 实际应用中,这里应该跳转到新功能页面 } + // 跳转到翻译统计页面 + void goToTranslationStats() { + Get.toNamed(Routes.translationStats); + } + @override void onClose() { searchController.dispose(); diff --git a/lib/modules/explore/views/explore_view.dart b/lib/modules/explore/views/explore_view.dart index 3f0fee73c..60779feed 100644 --- a/lib/modules/explore/views/explore_view.dart +++ b/lib/modules/explore/views/explore_view.dart @@ -115,12 +115,12 @@ class ExploreView extends GetView { ), child: IconButton( icon: Icon( - Icons.more_vert, + Icons.analytics_outlined, size: 16.sp, color: isDarkMode ? Colors.white : Colors.grey[700], ), padding: EdgeInsets.zero, - onPressed: () {}, + onPressed: controller.goToTranslationStats, ), ), ], @@ -419,37 +419,38 @@ class ExploreView extends GetView { //整行布局 Widget _buildAIFeaturesWideSection(bool isDarkMode) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Text( - // 'aiFunction'.tr, // AI功能 - // style: TextStyle( - // fontSize: 18.sp, - // fontWeight: FontWeight.w600, - // color: isDarkMode ? Colors.white : Colors.grey[800], - // ), - // ), - // SizedBox(height: 16.h), - // 直接使用 Column 包裹宽版卡片 - Column( - children: controller.aiFeaturesWide.map((feature) { - return Padding( - padding: EdgeInsets.only(bottom: 16.h), // 卡片间距 - child: _buildAIFeatureItemWide( - feature, - controller.aiFeatures.indexOf(feature), - isDarkMode, - ), - ); - }).toList(), - ), - ], - ); -} + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Text( + // 'aiFunction'.tr, // AI功能 + // style: TextStyle( + // fontSize: 18.sp, + // fontWeight: FontWeight.w600, + // color: isDarkMode ? Colors.white : Colors.grey[800], + // ), + // ), + // SizedBox(height: 16.h), + // 直接使用 Column 包裹宽版卡片 + Column( + children: controller.aiFeaturesWide.map((feature) { + return Padding( + padding: EdgeInsets.only(bottom: 16.h), // 卡片间距 + child: _buildAIFeatureItemWide( + feature, + controller.aiFeatures.indexOf(feature), + isDarkMode, + ), + ); + }).toList(), + ), + ], + ); + } - // AI功能项(宽版本,用于横跨整行的AI全能助手) - Widget _buildAIFeatureItemWide(AIFeature feature, int index, bool isDarkMode) { + // AI功能项(宽版本,用于横跨整行的AI全能助手) + Widget _buildAIFeatureItemWide( + AIFeature feature, int index, bool isDarkMode) { return GestureDetector( onTap: () => controller.onAIFeatureTap(index), child: Container( @@ -529,7 +530,8 @@ class ExploreView extends GetView { } // AI功能项(宽版本,专门用于商务会议助手) - Widget _buildAIFeatureItemWideForMeeting(AIFeature feature, int index, bool isDarkMode) { + Widget _buildAIFeatureItemWideForMeeting( + AIFeature feature, int index, bool isDarkMode) { return GestureDetector( onTap: () => controller.onAIFeatureWideTap(index), child: Container( @@ -646,7 +648,9 @@ class ExploreView extends GetView { child: Container( padding: EdgeInsets.all(16.w), decoration: BoxDecoration( - color: isDarkMode ? Colors.white.withValues(alpha: 0.05) : Colors.grey[100], + color: isDarkMode + ? Colors.white.withValues(alpha: 0.05) + : Colors.grey[100], borderRadius: BorderRadius.circular(16.r), boxShadow: [ BoxShadow( diff --git a/lib/modules/settings/views/settings_view.dart b/lib/modules/settings/views/settings_view.dart index b6fd93d6f..a65d7e441 100644 --- a/lib/modules/settings/views/settings_view.dart +++ b/lib/modules/settings/views/settings_view.dart @@ -407,28 +407,7 @@ class SettingsView extends GetView { ? Colors.white.withOpacity(0.1) : Colors.grey[200]), - // 翻译统计 - _buildSimpleNavigationSetting( - title: '翻译统计', // translationStats - subtitle: '查看翻译使用量和成本统计', // translationStatsInfo - icon: Icons.analytics_outlined, - iconBgColor: isDarkMode - ? Colors.green[900]!.withOpacity(0.3) - : Colors.green[100]!, - iconColor: - isDarkMode ? Colors.green[300]! : Colors.green[600]!, - titleColor: isDarkMode ? Colors.white : null, - subtitleColor: isDarkMode ? Colors.white70 : null, - onTap: () { - Get.toNamed(Routes.translationStats); - }, - isDarkMode: isDarkMode, - ), - Divider( - height: 1, - color: isDarkMode - ? Colors.white.withOpacity(0.1) - : Colors.grey[200]), + // 用户反馈 _buildSimpleNavigationSetting( From a4be8bc481037f2d9f6e1a6045a0218eca455936 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Sun, 20 Jul 2025 18:19:15 +0800 Subject: [PATCH 10/12] =?UTF-8?q?feat(ota=5Fupgrade):=20=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E6=8F=90=E7=A4=BA=E5=92=8CUI=E4=BA=A4?= =?UTF-8?q?=E4=BA=92=E4=BD=93=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 替换print为Logger输出日志 - 为所有snackbar添加统一样式和错误图标 - 改进电量不足和升级成功的对话框UI - 升级成功后添加跳转配对页面的引导 - 优化错误处理流程和用户提示 --- .../controllers/ota_upgrade_controller.dart | 257 ++++++++++++++++-- 1 file changed, 227 insertions(+), 30 deletions(-) diff --git a/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart b/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart index ef9bc97e8..907dd85f8 100644 --- a/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart +++ b/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart @@ -1,4 +1,6 @@ import 'package:classic_bluetooth/classic_bluetooth.dart'; +import '/routes/app_routes.dart'; +import 'package:logger/web.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:path/path.dart'; @@ -121,7 +123,17 @@ class OtaUpgradeController extends GetxController { // 实现更新逻辑... } catch (e) { errorMessage.value = e.toString(); - Get.snackbar('更新失败', e.toString()); + Get.snackbar( + '更新失败', + e.toString(), + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red[600], + colorText: Colors.white, + icon: const Icon( + Icons.error_outline, + color: Colors.white, + ), + ); } } @@ -166,7 +178,7 @@ class OtaUpgradeController extends GetxController { // 从第三个 `.` 开始分割 final firstPart = firmwareParts.sublist(0, 3).join('.'); final secondPart = firmwareParts.sublist(3).join('.'); - print("firstPart${firstPart},secondPart=${secondPart}"); + Logger().d("firstPart${firstPart},secondPart=${secondPart}"); // 判断两部分是否一致 if (firstPart != secondPart) { throw "固件版本格式错误:${deviceInfo.firmwareVersion}"; @@ -174,7 +186,7 @@ class OtaUpgradeController extends GetxController { // 正常流程 currentVersion.value = firstPart; - print('getequipmentinfo,id: ${deviceInfo.firmwareProductId}'); + Logger().d('getequipmentinfo,id: ${deviceInfo.firmwareProductId}'); final connectedDevices = await ClassicBluetooth.getConnectedDevices(); if (connectedDevices.isNotEmpty) { @@ -192,7 +204,7 @@ class OtaUpgradeController extends GetxController { } final resp = OtaConfigResp.fromJson(response); - print('getequipmentinfo,resp.systemversion: ${resp.systemversion}'); + Logger().d('getequipmentinfo,resp.systemversion: ${resp.systemversion}'); latestVersion.value = resp.systemversion!; updatedescription.value = resp.updatedescription!; updatepackageaddress = resp.updatepackageaddress!; @@ -210,10 +222,17 @@ class OtaUpgradeController extends GetxController { } catch (e) { // 统一错误处理 errorMessage.value = e.toString(); - Get.snackbar('错误', e.toString(), - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red, - colorText: Colors.white); + Get.snackbar( + '错误', + e.toString(), + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red[600], + colorText: Colors.white, + icon: const Icon( + Icons.error_outline, + color: Colors.white, + ), + ); // 重要:重新抛出异常以便上层处理(可选) } finally { isFetchingDeviceInfo.value = false; @@ -237,7 +256,7 @@ class OtaUpgradeController extends GetxController { } return false; } catch (e) { - print('版本比较错误: $e'); + Logger().d('版本比较错误: $e'); return false; } } @@ -248,15 +267,35 @@ class OtaUpgradeController extends GetxController { if (latestVersion.value != "") { _showUpdateDialog(latestVersion.value); } else { - Get.snackbar('错误', '获取最新版本信息失败'); + Get.snackbar( + '错误', + '获取最新版本信息失败', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange[800], + colorText: Colors.white, + icon: const Icon( + Icons.error_outline, + color: Colors.white, + ), + ); } } catch (e) { - Get.snackbar('错误', '获取版本信息失败'); + Get.snackbar( + '错误', + '获取版本信息失败', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange[800], + colorText: Colors.white, + icon: const Icon( + Icons.error_outline, + color: Colors.white, + ), + ); } } void _showUpdateDialog(String latest) async { - print('显示更新对话框'); + Logger().d('显示更新对话框'); Get.dialog( AlertDialog( title: Text('发现新版本 $latest'), @@ -264,7 +303,7 @@ class OtaUpgradeController extends GetxController { actions: [ TextButton( onPressed: () async { - print('取消按钮被点击'); + Logger().d('取消按钮被点击'); Get.back(); Future.delayed(Duration(milliseconds: 300)); @@ -273,7 +312,7 @@ class OtaUpgradeController extends GetxController { ), TextButton( onPressed: () async { - print('更新按钮被点击'); + Logger().d('更新按钮被点击'); Get.back(); Future.delayed(Duration(milliseconds: 300)); @@ -341,7 +380,17 @@ class OtaUpgradeController extends GetxController { } } } catch (e) { - Get.snackbar('错误', '检查本地文件失败: $e'); + Get.snackbar( + '错误', + '检查本地文件失败: $e', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red[600], + colorText: Colors.white, + icon: Icon( + Icons.error_outline, + color: Colors.white, + ), + ); return; } @@ -363,12 +412,22 @@ class OtaUpgradeController extends GetxController { } } } catch (e) { - Get.snackbar('错误', '删除旧文件失败: $e'); + Get.snackbar( + '错误', + '删除旧文件失败: $e', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red[600], + colorText: Colors.white, + icon: Icon( + Icons.error_outline, + color: Colors.white, + ), + ); //isUpgrade.value = false; return; } - print('文件保存路径: $savePath'); + Logger().d('文件保存路径: $savePath'); detailedStatus.value = '开始下载固件包...'; final options = Options( @@ -394,7 +453,17 @@ class OtaUpgradeController extends GetxController { // Get.snackbar('下载完成', '文件已保存至: $savePath'); detailedStatus.value = '下载完成,准备升级...'; } else { - Get.snackbar('错误', '文件未找到,请检查写入权限'); + Get.snackbar( + '错误', + '文件未找到,请检查写入权限', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange[600], + colorText: Colors.white, + icon: Icon( + Icons.error_outline, + color: Colors.white, + ), + ); detailedStatus.value = '下载失败'; showDetailedStatus.value = false; return; @@ -403,15 +472,45 @@ class OtaUpgradeController extends GetxController { startUpgrading(savePath); return; } on DioException catch (e) { - Get.snackbar('下载失败', '错误: ${e.message}'); + Get.snackbar( + '下载失败', + '错误: ${e.message}', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange[600], + colorText: Colors.white, + icon: Icon( + Icons.error_outline, + color: Colors.white, + ), + ); } on FileSystemException catch (e) { - Get.snackbar('存储失败', '无法写入文件: ${e.message}'); + Get.snackbar( + '存储失败', + '无法写入文件: ${e.message}', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange[600], + colorText: Colors.white, + icon: Icon( + Icons.error_outline, + color: Colors.white, + ), + ); } catch (e) { isDownloading.value = false; detailedStatus.value = '下载失败,请检查网络连接'; showDetailedStatus.value = false; - Get.snackbar('下载失败', '请检查网络连接: $e'); - print('下载失败: $e'); + Get.snackbar( + '下载失败', + '请检查网络连接: $e', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange[600], + colorText: Colors.white, + icon: Icon( + Icons.error_outline, + color: Colors.white, + ), + ); + Logger().d('下载失败: $e'); } } @@ -442,8 +541,12 @@ class OtaUpgradeController extends GetxController { '电量检查失败', '无法获取设备信息,请检查设备连接', snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.orange, + backgroundColor: Colors.orange[600], colorText: Colors.white, + icon: Icon( + Icons.warning_amber, + color: Colors.white, + ), ); return false; } @@ -460,14 +563,36 @@ class OtaUpgradeController extends GetxController { Get.dialog( AlertDialog( - title: Text('电量不足'), - content: Text(message), + title: Text( + '电量不足', + style: TextStyle( + color: Colors.orange[700], + fontWeight: FontWeight.w600, + ), + ), + content: Text( + message, + style: TextStyle( + color: Colors.orange[600], + ), + ), actions: [ TextButton( onPressed: () => Get.back(), - child: Text('确定'), + style: TextButton.styleFrom( + foregroundColor: Colors.orange[700], + ), + child: const Text('确定'), ), ], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide( + color: Colors.orange.withValues(alpha: 0.3), + width: 1, + ), + ), + backgroundColor: Colors.orange[50], ), ); return false; @@ -479,8 +604,12 @@ class OtaUpgradeController extends GetxController { '电量检查失败', '检查设备电量时发生错误:$e', snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red, + backgroundColor: Colors.orange[600], colorText: Colors.white, + icon: const Icon( + Icons.warning_amber, + color: Colors.white, + ), ); return false; } @@ -507,15 +636,83 @@ class OtaUpgradeController extends GetxController { isUpgrade.value = false; detailedStatus.value = '升级成功完成'; showDetailedStatus.value = false; - Get.snackbar('升级成功', '请退出重新连接设备'); - _getCurrentVersion(); + + // 升级成功,更新版本信息显示 + currentVersion.value = latestVersion.value; + isUpdateAvailable.value = false; + + // 显示升级成功对话框,引导用户重新配对 + Get.dialog( + AlertDialog( + title: Row( + children: [ + Icon( + Icons.check_circle, + color: Colors.green[600], + size: 28, + ), + SizedBox(width: 12), + Text( + '升级成功', + style: TextStyle( + color: Colors.green[700], + fontWeight: FontWeight.w600, + ), + ), + ], + ), + content: Text( + '固件升级已完成!\n\n设备需要重新配对才能正常使用,现在跳转到配对页面吗?', + style: TextStyle( + fontSize: 16, + height: 1.4, + ), + ), + actions: [ + TextButton( + onPressed: () { + Get.back(); // 关闭对话框 + }, + child: Text( + '稍后配对', + style: TextStyle(color: Colors.grey[600]), + ), + ), + ElevatedButton( + onPressed: () { + Get.back(); // 关闭对话框 + Get.toNamed(Routes.pairing); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green[600], + foregroundColor: Colors.white, + ), + child: Text('立即配对'), + ), + ], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + barrierDismissible: false, // 防止用户意外关闭 + ); } if (event.event == 'onError') { type.value = UPGRADE_FAILED; isUpgrade.value = false; detailedStatus.value = '升级失败,错误码:${event.code}'; showDetailedStatus.value = false; - Get.snackbar('升级失败', '请检查设备连接或升级包,错误码:${event.code}'); + Get.snackbar( + '升级失败', + '请检查设备连接或升级包,错误码:${event.code}', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red[600], + colorText: Colors.white, + icon: Icon( + Icons.error_outline, + color: Colors.white, + ), + ); } if (event.event == 'NOCONNECTION') { type.value = CONNECTION_DISCONNECTION; From d9af678ac195b5536ea2991aacac058ba0da5934 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Sun, 20 Jul 2025 20:45:56 +0800 Subject: [PATCH 11/12] =?UTF-8?q?feat(pairing):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E9=85=8D=E5=AF=B9=E8=A7=86=E5=9B=BE=E5=8A=A8=E7=94=BB=E6=95=88?= =?UTF-8?q?=E6=9E=9C=E5=92=8C=E6=8E=A7=E5=88=B6=E5=99=A8=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构配对视图动画系统,使用更高效的AnimationController实现连续脉搏和旋转效果 新增安全连接和设备关联的特殊动画效果 优化控制器检查逻辑,使用Future.wait并行执行蓝牙和权限检查 调整导航延迟从1秒缩短至500毫秒 更新颜色透明度方法从withOpacity改为withValues --- .gitignore | 1 + .../controllers/pairing_controller.dart | 29 +- lib/modules/pairing/views/pairing_view.dart | 342 +++++++++++++++--- .../pairing/views/pairing_view_new.dart | 7 +- 4 files changed, 314 insertions(+), 65 deletions(-) diff --git a/.gitignore b/.gitignore index 51e5178e8..80c4856c3 100644 --- a/.gitignore +++ b/.gitignore @@ -221,3 +221,4 @@ fastlane/test_output fastlane/readme.md .trae/rules/project_rules.md .augment/rules/imported/project_rules.md +.kiro/steering/flutter_project_standards.md diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index 280f8cf58..21c6d33c8 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -384,29 +384,33 @@ class PairingController extends GetxController { /// 检查当前状态并决定下一步 void _checkCurrentStatus() async { try { - // 1. 检查蓝牙状态 - if (_bluetoothManager.bluetoothState == 'off') { + // 使用 Future.wait 并行执行检查,提升启动速度 + final results = await Future.wait([ + _checkBluetoothState(), // 检查蓝牙状态 + _checkPermissions(), // 检查权限 + _bluetoothManager.refreshDeviceConnection(), // 刷新设备连接 + ]); + + final isBluetoothOk = results[0] as bool; + final arePermissionsOk = results[1] as bool; + + if (!isBluetoothOk) { _setError(ErrorType.bluetoothOff, 'pleaseEnableBluetooth'.tr); return; } - // 2. 检查权限 - if (!await _checkPermissions()) { + if (!arePermissionsOk) { _setError(ErrorType.noPermission, 'pleaseGrantNecessaryPermissions'.tr); return; } - // 3. 先刷新设备连接状态,确保获取最新的连接信息 - await _bluetoothManager.refreshDeviceConnection(); - - // 4. 检查经典蓝牙连接 + // 后续逻辑保持不变,确保所有设备都经过验证 final classicDevice = _bluetoothManager.currentDevice; if (classicDevice == null) { _transition(PairingState.waitingClassic); return; } - // 5. 如果有设备,进行验证 _currentClassicDevice = classicDevice; _transition(PairingState.verifying); await _verifyDevice(classicDevice); @@ -416,6 +420,11 @@ class PairingController extends GetxController { } } + /// 新增一个辅助方法来检查蓝牙状态 + Future _checkBluetoothState() async { + return _bluetoothManager.bluetoothState != 'off'; + } + /// 检查权限 Future _checkPermissions() async { // Android权限检查 @@ -700,7 +709,7 @@ class PairingController extends GetxController { /// 导航到主页 void _navigateToHome() { - Future.delayed(const Duration(seconds: 1), () async { + Future.delayed(const Duration(milliseconds: 500), () async { // 启动AgentService服务 try { Logger.i(_tag, '启动AgentService服务'); diff --git a/lib/modules/pairing/views/pairing_view.dart b/lib/modules/pairing/views/pairing_view.dart index 1f2bf9b31..a059ab6a8 100644 --- a/lib/modules/pairing/views/pairing_view.dart +++ b/lib/modules/pairing/views/pairing_view.dart @@ -3,9 +3,68 @@ import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../controllers/pairing_controller.dart'; -class PairingView extends GetView { +class PairingView extends StatefulWidget { const PairingView({Key? key}) : super(key: key); + @override + State createState() => _PairingViewState(); +} + +class _PairingViewState extends State + with TickerProviderStateMixin { + late AnimationController _pulseController; + late AnimationController _rotationController; + late Animation _pulseAnimation; + late Animation _rotationAnimation; + + PairingController get controller => Get.find(); + + @override + void initState() { + super.initState(); + + // 脉搏动画控制器 + _pulseController = AnimationController( + duration: const Duration(milliseconds: 1500), + vsync: this, + ); + + // 旋转动画控制器 + _rotationController = AnimationController( + duration: const Duration(milliseconds: 2000), + vsync: this, + ); + + // 脉搏动画 + _pulseAnimation = Tween( + begin: 0.8, + end: 1.2, + ).animate(CurvedAnimation( + parent: _pulseController, + curve: Curves.easeInOut, + )); + + // 旋转动画 + _rotationAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _rotationController, + curve: Curves.linear, + )); + + // 开始动画 + _pulseController.repeat(reverse: true); + _rotationController.repeat(); + } + + @override + void dispose() { + _pulseController.dispose(); + _rotationController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final bool isDarkMode = Theme.of(context).brightness == Brightness.dark; @@ -30,7 +89,7 @@ class PairingView extends GetView { padding: EdgeInsets.symmetric(horizontal: 24.w), // 添加SingleChildScrollView以防止溢出 child: SingleChildScrollView( - physics: BouncingScrollPhysics(), + physics: const BouncingScrollPhysics(), child: ConstrainedBox( constraints: BoxConstraints( minHeight: @@ -98,8 +157,8 @@ class PairingView extends GetView { final state = controller.currentState; final isLoading = controller.isLoading.value; // 根据状态决定动画颜色 - Color primaryColor = Color(0xFF0066CC); - Color secondaryColor = Color(0xFF5E5CE6); + Color primaryColor = const Color(0xFF0066CC); + Color secondaryColor = const Color(0xFF5E5CE6); // 错误状态使用红色 if (state == PairingState.error) { @@ -114,7 +173,7 @@ class PairingView extends GetView { // 待机状态的特殊处理 if (state == PairingState.idle || state == PairingState.waitingClassic) { - return Container( + return SizedBox( width: 240.w, height: 240.w, child: Stack( @@ -129,8 +188,8 @@ class PairingView extends GetView { begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ - primaryColor.withOpacity(0.1), - secondaryColor.withOpacity(0.1), + primaryColor.withValues(alpha: 0.1), + secondaryColor.withValues(alpha: 0.1), ], ), shape: BoxShape.circle, @@ -159,51 +218,25 @@ class PairingView extends GetView { ); } - return Container( + return SizedBox( width: 240.w, height: 240.w, child: Stack( alignment: Alignment.center, children: [ - // 脉搏背景 - 只在加载时显示动画 - if (isLoading) - TweenAnimationBuilder( - tween: Tween(begin: 0.8, end: 1.1), - duration: const Duration(seconds: 2), - curve: Curves.easeInOut, - builder: (context, value, child) { - return Container( - width: 200.w * value, - height: 200.w * value, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - primaryColor.withOpacity(0.3), - secondaryColor.withOpacity(0.3), - ], - ), - shape: BoxShape.circle, - ), - ); - }, - onEnd: () { - // 重新开始动画 - }, - ), + // 连续脉搏动画 - 只在加载时显示 + if (isLoading) _buildPulseAnimation(primaryColor, secondaryColor), + + // 特殊的安全连接动画 - 只在建立安全连接时显示 + if (state == PairingState.waitingBle) + _buildSecureConnectionAnimation(primaryColor, secondaryColor), + + // 设备关联动画 - 只在关联设备时显示 + if (state == PairingState.associating) + _buildAssociatingAnimation(primaryColor, secondaryColor), // 旋转边框 - 只在加载时显示 - if (isLoading) - SizedBox( - width: 100.w, - height: 100.w, - child: CircularProgressIndicator( - strokeWidth: 4.w, - valueColor: AlwaysStoppedAnimation(primaryColor), - backgroundColor: Colors.grey.withOpacity(0.2), - ), - ), + if (isLoading) _buildRotatingBorder(primaryColor), // 设备图标 Container( @@ -229,6 +262,128 @@ class PairingView extends GetView { }); } + // 构建连续脉搏动画 + Widget _buildPulseAnimation(Color primaryColor, Color secondaryColor) { + return AnimatedBuilder( + animation: _pulseAnimation, + builder: (context, child) { + final value = _pulseAnimation.value; + final opacity = 0.4 - (_pulseController.value * 0.3); // 0.4 到 0.1 的透明度变化 + + return Container( + width: 200.w * value, + height: 200.w * value, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + primaryColor.withValues(alpha: opacity), + secondaryColor.withValues(alpha: opacity), + ], + ), + shape: BoxShape.circle, + ), + ); + }, + ); + } + + // 构建旋转边框动画 + Widget _buildRotatingBorder(Color primaryColor) { + return AnimatedBuilder( + animation: _rotationAnimation, + builder: (context, child) { + return Transform.rotate( + angle: _rotationAnimation.value * 2 * 3.14159, // 完整旋转 + child: SizedBox( + width: 140.w, + height: 140.w, + child: CircularProgressIndicator( + strokeWidth: 3.w, + valueColor: AlwaysStoppedAnimation(primaryColor), + backgroundColor: Colors.grey.withValues(alpha: 0.2), + value: 0.7, // 显示70%的圆弧 + ), + ), + ); + }, + ); + } + + // 构建安全连接特殊动画 - 多层波纹效果 + Widget _buildSecureConnectionAnimation(Color primaryColor, Color secondaryColor) { + return Stack( + alignment: Alignment.center, + children: [ + // 第一层波纹 + AnimatedBuilder( + animation: _pulseAnimation, + builder: (context, child) { + final value = _pulseAnimation.value; + final opacity = 0.3 - (_pulseController.value * 0.2); + + return Container( + width: 180.w * value, + height: 180.w * value, + decoration: BoxDecoration( + border: Border.all( + color: primaryColor.withValues(alpha: opacity), + width: 2.w, + ), + shape: BoxShape.circle, + ), + ); + }, + ), + + // 第二层波纹 - 延迟动画 + AnimatedBuilder( + animation: _pulseAnimation, + builder: (context, child) { + final delayedValue = (_pulseController.value + 0.3) % 1.0; + final value = 0.8 + (delayedValue * 0.4); + final opacity = 0.25 - (delayedValue * 0.15); + + return Container( + width: 160.w * value, + height: 160.w * value, + decoration: BoxDecoration( + border: Border.all( + color: secondaryColor.withValues(alpha: opacity), + width: 1.5.w, + ), + shape: BoxShape.circle, + ), + ); + }, + ), + + // 第三层波纹 - 更大延迟 + AnimatedBuilder( + animation: _pulseAnimation, + builder: (context, child) { + final delayedValue = (_pulseController.value + 0.6) % 1.0; + final value = 0.8 + (delayedValue * 0.4); + final opacity = 0.2 - (delayedValue * 0.1); + + return Container( + width: 140.w * value, + height: 140.w * value, + decoration: BoxDecoration( + border: Border.all( + color: primaryColor.withValues(alpha: opacity), + width: 1.w, + ), + shape: BoxShape.circle, + ), + ); + }, + ), + ], + ); + } + // 根据状态获取图标 IconData _getIconForState(PairingState state) { switch (state) { @@ -352,7 +507,7 @@ class PairingView extends GetView { // 已连接设备信息 Widget _buildConnectedDevice(device, bool isDarkMode) { - return Container( + return SizedBox( width: double.infinity, child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -443,7 +598,7 @@ class PairingView extends GetView { final showSkipButton = controller.showSkipButton.value; final showAutoCodeInput = controller.showAutoCodeInput.value; final showSkipBindingButton = controller.showSkipBindingButton.value; - return Container( + return SizedBox( width: double.infinity, child: Column( children: [ @@ -452,7 +607,7 @@ class PairingView extends GetView { ElevatedButton( onPressed: () => controller.onConnectTap(), style: ElevatedButton.styleFrom( - backgroundColor: Color(0xFF0066CC), + backgroundColor: const Color(0xFF0066CC), foregroundColor: Colors.white, padding: EdgeInsets.symmetric(vertical: 16.h), shape: RoundedRectangleBorder( @@ -572,7 +727,7 @@ class PairingView extends GetView { TextButton( onPressed: () => controller.onSkipTap(), style: TextButton.styleFrom( - foregroundColor: Color(0xFF0066CC), + foregroundColor: const Color(0xFF0066CC), padding: EdgeInsets.symmetric(vertical: 12.h), minimumSize: Size(double.infinity, 48.h), ), @@ -592,7 +747,7 @@ class PairingView extends GetView { TextButton( onPressed: () => controller.onSkipBindingTap(), style: TextButton.styleFrom( - foregroundColor: Color(0xFF0066CC), + foregroundColor: const Color(0xFF0066CC), padding: EdgeInsets.symmetric(vertical: 12.h), minimumSize: Size(double.infinity, 48.h), ), @@ -611,7 +766,7 @@ class PairingView extends GetView { TextButton( onPressed: () => controller.onRetryTap(), style: TextButton.styleFrom( - foregroundColor: Color(0xFF0066CC), + foregroundColor: const Color(0xFF0066CC), padding: EdgeInsets.symmetric(vertical: 12.h), minimumSize: Size(double.infinity, 48.h), ), @@ -629,4 +784,89 @@ class PairingView extends GetView { ); }); } + + // 构建设备关联动画 - 连接线条效果 + Widget _buildAssociatingAnimation(Color primaryColor, Color secondaryColor) { + return AnimatedBuilder( + animation: _rotationAnimation, + builder: (context, child) { + return Stack( + alignment: Alignment.center, + children: [ + // 旋转的连接线条 + Transform.rotate( + angle: _rotationAnimation.value * 2 * 3.14159, + child: Container( + width: 160.w, + height: 160.w, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: primaryColor.withValues(alpha: 0.6), + width: 2.w, + ), + ), + child: Stack( + children: [ + // 连接点1 + Positioned( + top: 10.w, + left: 80.w - 4.w, + child: Container( + width: 8.w, + height: 8.w, + decoration: BoxDecoration( + color: primaryColor, + shape: BoxShape.circle, + ), + ), + ), + // 连接点2 + Positioned( + bottom: 10.w, + left: 80.w - 4.w, + child: Container( + width: 8.w, + height: 8.w, + decoration: BoxDecoration( + color: secondaryColor, + shape: BoxShape.circle, + ), + ), + ), + // 连接点3 + Positioned( + top: 80.w - 4.w, + left: 10.w, + child: Container( + width: 8.w, + height: 8.w, + decoration: BoxDecoration( + color: primaryColor, + shape: BoxShape.circle, + ), + ), + ), + // 连接点4 + Positioned( + top: 80.w - 4.w, + right: 10.w, + child: Container( + width: 8.w, + height: 8.w, + decoration: BoxDecoration( + color: secondaryColor, + shape: BoxShape.circle, + ), + ), + ), + ], + ), + ), + ), + ], + ); + }, + ); + } } diff --git a/lib/modules/pairing/views/pairing_view_new.dart b/lib/modules/pairing/views/pairing_view_new.dart index acb400810..b4a2f94d2 100644 --- a/lib/modules/pairing/views/pairing_view_new.dart +++ b/lib/modules/pairing/views/pairing_view_new.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../controllers/pairing_controller.dart'; -import '../../../core/themes/app_theme.dart'; /// 配对视图 - 简化版本 class PairingView extends GetView { @@ -94,9 +93,9 @@ class PairingView extends GetView { Container( padding: EdgeInsets.all(16.w), decoration: BoxDecoration( - color: Colors.red.withOpacity(0.1), + color: Colors.red.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12.r), - border: Border.all(color: Colors.red.withOpacity(0.3)), + border: Border.all(color: Colors.red.withValues(alpha: 0.3)), ), child: Row( children: [ @@ -184,7 +183,7 @@ class PairingView extends GetView { width: 100.w, height: 100.w, decoration: BoxDecoration( - color: iconColor.withOpacity(0.1), + color: iconColor.withValues(alpha: 0.1), shape: BoxShape.circle, ), child: Icon( From d432974a95cd82216c4f8c63ba6343c1849903ef Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Sun, 20 Jul 2025 23:44:18 +0800 Subject: [PATCH 12/12] =?UTF-8?q?feat(=E8=AF=AD=E9=9F=B3=E8=AF=86=E5=88=AB?= =?UTF-8?q?):=20=E6=B7=BB=E5=8A=A0agent=E6=A8=A1=E5=9D=97=E5=A4=9A?= =?UTF-8?q?=E8=AF=AD=E8=A8=80=E6=94=AF=E6=8C=81=E9=85=8D=E7=BD=AE=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现语音识别多语言动态配置,支持根据APP语言环境自动设置 新增setSupportedLanguages方法用于设置支持的语言列表 默认添加en-US作为备选语言 --- .../agent/controllers/agent_controller.dart | 42 ++++++++++++ .../agent_service/AgentService.kt | 64 +++++++++++++++++++ .../agent_service/AgentServicePlugin.kt | 16 +++++ .../agent_service/AgentServiceImpl.swift | 3 + .../agent_service/lib/agent_service.dart | 15 +++++ .../azure_speech/AzureAsrHelper.kt | 2 +- 6 files changed, 141 insertions(+), 1 deletion(-) diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index 08bdffa81..22eaa937b 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -152,6 +152,48 @@ class AgentController extends GetxController { _initMusicStatus(); //初始化音乐状态 _setupMediaSessionCallbacks(); //设置媒体会话回调 _setupTextInputListener(); //设置文本输入监听器 + + try { + // 1. 使用 GetX 获取 APP 当前语言环境 (Locale),而不是设备语言 + final appLocale = Get.locale; + List languagesToSupport = []; + + if (appLocale != null) { + final lang = appLocale.languageCode; // e.g., 'en' + final country = appLocale.countryCode; // e.g., 'US' + + // 2. 构造 Azure SDK 需要的格式 (e.g., 'en-US') + if (country != null && country.isNotEmpty) { + languagesToSupport.add('$lang-$country'); + } else { + languagesToSupport.add(lang); // fallback only language code + } + } + + // 3. 添加 en-US 作为备选 + if (!languagesToSupport.contains('en-US')) { + languagesToSupport.add('en-US'); + } + + // 4. 最终兜底 + if (languagesToSupport.isEmpty) { + languagesToSupport.add('en-US'); + } + + // 5. 调用原生设置 + Logger.i(TAG, '支持的语言: ${languagesToSupport.join(', ')}'); + AgentService.setSupportedLanguages(languagesToSupport).then((success) { + if (success) { + Logger.i(TAG, '支持的语言已根据 APP 设置语言: ${languagesToSupport.join(', ')}'); + } else { + Logger.w(TAG, '根据 APP 设置语言失败'); + } + }); + } catch (e) { + Logger.e(TAG, "获取 APP 语言并设置失败: $e"); + AgentService.setSupportedLanguages(['zh-CN', 'en-US']); + } + startVoiceInput(); //开启语音输入 } diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index 37ebe489e..1437d4982 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -52,6 +52,9 @@ interface AgentServiceListener { object AgentService : CoroutineScope { private val TAG = "AgentService" + private var azureSpeechKey: String = "" + private var azureSpeechRegion: String = "" + // 协程相关 private val job = SupervisorJob() override val coroutineContext: CoroutineContext @@ -179,6 +182,9 @@ object AgentService : CoroutineScope { LocationService.initialize(appContext) // 设置系统提示词 val customSystemPrompt = config["systemPrompt"] as? String + //保存配置 + this.azureSpeechKey = config["azureSpeechKey"]?.toString() ?: "" + this.azureSpeechRegion = config["azureSpeechRegion"]?.toString() ?: "" if (!customSystemPrompt.isNullOrBlank()) { systemPrompt = customSystemPrompt } else { @@ -188,9 +194,15 @@ object AgentService : CoroutineScope { azureAsrHelper = AzureAsrHelper(context).apply { // 使用协程处理异步初始化 CoroutineScope(Dispatchers.IO).launch { + //获取语音识别的语种 + val supportedLanguagesConfig = config["supportedLanguages"] as? List + val supportedLanguages = supportedLanguagesConfig?.toTypedArray() + ?: arrayOf("zh-CN", "en-US") // 默认支持多语言 + val asrInitSuccess = initialize( subscriptionKey = config["azureSpeechKey"]?.toString() ?: "", region = config["azureSpeechRegion"]?.toString() ?: "", + supportedLanguages = supportedLanguages, audioSourceType = if (isExternalActive) AzureAsrHelper.AudioSourceType.EXTERNAL else AzureAsrHelper.AudioSourceType.MICROPHONE ) @@ -1091,6 +1103,58 @@ object AgentService : CoroutineScope { } } + /** + * 设置支持的语音识别语言 + */ + fun setSupportedLanguages(languages: List): Boolean { + if (!isInitialized) { + Log.e(TAG, "服务未初始化") + sendEvent("error", mapOf("code" to "NOT_INITIALIZED", "message" to "服务未初始化")) + return false + } + + if (languages.isEmpty()) { + Log.e(TAG, "语言列表不能为空") + sendEvent("error", mapOf("code" to "EMPTY_LANGUAGES", "message" to "语言列表不能为空")) + return false + } + + return try { + Log.i(TAG, "设置支持的语言: ${languages.joinToString(", ")}") + + // 重新初始化ASR服务以应用新的语言设置 + val success = azureAsrHelper?.initialize( + subscriptionKey = this.azureSpeechKey, + region = this.azureSpeechRegion, + supportedLanguages = languages.toTypedArray(), + audioSourceType = audioSourceType + ) ?: false + + if (success) { + Log.i(TAG, "语言设置更新成功") + sendEvent("languages_updated", mapOf( + "languages" to languages, + "status" to "success" + )) + } else { + Log.e(TAG, "语言设置更新失败") + sendEvent("error", mapOf( + "code" to "LANGUAGE_UPDATE_FAILED", + "message" to "语言设置更新失败" + )) + } + + success + } catch (e: Exception) { + Log.e(TAG, "设置支持语言异常: ${e.message}") + sendEvent("error", mapOf( + "code" to "LANGUAGE_UPDATE_ERROR", + "message" to e.message.toString() + )) + false + } + } + /** * 保存聊天消息 */ diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt index 1a5d06561..8229f4863 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt @@ -201,6 +201,22 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream } } + "setSupportedLanguages" -> { + val languages = call.argument>("languages") + if (languages.isNullOrEmpty()) { + result.error("INVALID_ARGUMENTS", "语言列表不能为空", null) + return + } + + try { + val isSuccess = AgentService.setSupportedLanguages(languages) + result.success(isSuccess) + } catch (e: Exception) { + Log.e(TAG, "设置支持语言失败", e) + result.error("SET_LANGUAGES_ERROR", "设置支持语言失败: ${e.message}", null) + } + } + "stopAiSteam" -> { try { AgentService.stopAiStream() diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift index 0dc9119f6..b817d7713 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift @@ -196,6 +196,9 @@ class AgentServiceImpl: NSObject { private func initializeAzureSpeech() -> Bool { azureTtsHelper?.addListener(self) + + // 获取支持的语言列表,支持从配置中读取 + let supportedLanguages = config["supportedLanguages"] as? [String] ?? ["zh-CN", "en-US"] guard let asrSuccess = azureAsrHelper?.initialize( subscriptionKey: azureSpeechKey, diff --git a/local_plugins/agent_service/lib/agent_service.dart b/local_plugins/agent_service/lib/agent_service.dart index 664b3faad..ede6b56dc 100644 --- a/local_plugins/agent_service/lib/agent_service.dart +++ b/local_plugins/agent_service/lib/agent_service.dart @@ -332,6 +332,21 @@ class AgentService { } } + /// 设置支持的语音识别语言 + /// + /// [languages] 支持的语言列表,如 ['zh-CN', 'en-US'] + /// 返回是否设置成功 + static Future setSupportedLanguages(List languages) async { + try { + final bool result = await _channel.invokeMethod('setSupportedLanguages', { + 'languages': languages, + }); + return result; + } on PlatformException catch (e) { + throw AgentServiceException(e.code, e.message ?? '设置支持语言失败', e.details); + } + } + /// 停止语音合成 static Future stopAiSteam() async { try { diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt index ce143a45c..17e073b0a 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt @@ -87,7 +87,7 @@ class AzureAsrHelper(private val context: Context) { fun initialize( subscriptionKey: String, region: String, - supportedLanguages: Array = arrayOf("zh-CN"), + supportedLanguages: Array = arrayOf("zh-CN", "en-US"), audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE ): Boolean { try {