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] =?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'; // 面对面翻译