Browse Source

feat(translation): 新增翻译使用统计功能

- 添加翻译统计页面及相关路由配置
- 实现翻译、ASR、TTS API调用统计功能
- 新增成本计算和预警机制
- 完善统计数据的存储和展示逻辑
weicu
lxm 1 year ago
parent
commit
4b48983c9b
  1. 450
      lib/data/models/translation_usage_stats.dart
  2. 519
      lib/data/services/translation_usage_service.dart
  3. 23
      lib/modules/settings/views/settings_view.dart
  4. 18
      lib/modules/translation/bindings/translation_stats_binding.dart
  5. 162
      lib/modules/translation/controllers/translation_controller.dart
  6. 258
      lib/modules/translation/controllers/translation_cost_controller.dart
  7. 25
      lib/modules/translation/controllers/translation_detail_controller.dart
  8. 358
      lib/modules/translation/controllers/translation_stats_controller.dart
  9. 757
      lib/modules/translation/views/translation_stats_view.dart
  10. 9
      lib/routes/app_pages.dart
  11. 1
      lib/routes/app_routes.dart

450
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<String, dynamic> 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<String, dynamic> 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<String, dynamic> json) {
return LanguagePairStats(
languagePair: json['languagePair'] ?? '',
translationCount: json['translationCount'] ?? 0,
sourceCharacters: json['sourceCharacters'] ?? 0,
cost: (json['cost'] ?? 0.0).toDouble(),
);
}
Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, TranslationModeStats> modeStats; // 按翻译模式分类统计
final Map<String, LanguagePairStats> languagePairStats; // 按语言对分类统计
final Map<String, HourlyStats> hourlyStats; // 按小时分类统计(仅日统计包含)
// 成本相关
final double estimatedCost; // 预估成本
final String currency; // 货币单位
final Map<String, double> 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<String, dynamic> json) {
// 处理多维度统计数据的反序列化
final modeStatsMap = <String, TranslationModeStats>{};
if (json['modeStats'] != null) {
final modeStatsJson = json['modeStats'] as Map<String, dynamic>;
modeStatsJson.forEach((key, value) {
modeStatsMap[key] = TranslationModeStats.fromJson(value);
});
}
final languagePairStatsMap = <String, LanguagePairStats>{};
if (json['languagePairStats'] != null) {
final languagePairStatsJson = json['languagePairStats'] as Map<String, dynamic>;
languagePairStatsJson.forEach((key, value) {
languagePairStatsMap[key] = LanguagePairStats.fromJson(value);
});
}
final hourlyStatsMap = <String, HourlyStats>{};
if (json['hourlyStats'] != null) {
final hourlyStatsJson = json['hourlyStats'] as Map<String, dynamic>;
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<String, double>.from(json['costBreakdown'] ?? {}),
);
}
Map<String, dynamic> toJson() {
// 序列化多维度统计数据
final modeStatsJson = <String, dynamic>{};
modeStats.forEach((key, value) {
modeStatsJson[key] = value.toJson();
});
final languagePairStatsJson = <String, dynamic>{};
languagePairStats.forEach((key, value) {
languagePairStatsJson[key] = value.toJson();
});
final hourlyStatsJson = <String, dynamic>{};
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<String, TranslationModeStats>? modeStats,
Map<String, LanguagePairStats>? languagePairStats,
Map<String, HourlyStats>? hourlyStats,
double? estimatedCost,
String? currency,
Map<String, double>? 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<String, int> get languagePairUsage {
final result = <String, int>{};
languagePairStats.forEach((key, value) {
result[key] = value.translationCount;
});
return result;
}
/// @deprecated 使用 modeStats 替代
Map<String, int> get modeUsage {
final result = <String, int>{};
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;
}

519
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<RealtimeUsageStats> realtimeStats = RealtimeUsageStats().obs;
// 当日统计数据
final Rx<TranslationUsageStats> todayStats = TranslationUsageStats.empty('').obs;
// 成本配置
final RxMap<String, double> costConfig = <String, double>{}.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<String, double>.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<String, dynamic>.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<String, LanguagePairStats>.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<String, TranslationModeStats>.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<String, TranslationModeStats>.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<List<TranslationUsageStats>> getHistoryStats({
required DateTime startDate,
required DateTime endDate,
}) async {
try {
final List<TranslationUsageStats> 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<String, dynamic>.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<String, double> 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<TranslationUsageStats> 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<String, int> combinedLanguagePairUsage = {};
final Map<String, int> 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());
}
}
}

23
lib/modules/settings/views/settings_view.dart

@ -407,6 +407,29 @@ class SettingsView extends GetView<SettingsController> {
? Colors.white.withOpacity(0.1) ? Colors.white.withOpacity(0.1)
: Colors.grey[200]), : 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( _buildSimpleNavigationSetting(
title: 'feedback'.tr, // 用户反馈 title: 'feedback'.tr, // 用户反馈

18
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<TranslationUsageService>()) {
Get.put(TranslationUsageService(), permanent: true);
}
// 注册控制器
Get.lazyPut<TranslationStatsController>(
() => TranslationStatsController(),
);
}
}

162
lib/modules/translation/controllers/translation_controller.dart

@ -1,4 +1,3 @@
import 'dart:ffi';
import 'dart:io'; import 'dart:io';
import '../../../core/utils/permission_util.dart'; 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/tts_service.dart';
import '../../../data/services/language_manager.dart'; import '../../../data/services/language_manager.dart';
import '../../../core/utils/logger.dart'; import '../../../core/utils/logger.dart';
import '../../../data/models/events/voice_interaction_event.dart';
import '../../../data/services/asr_service.dart'; import '../../../data/services/asr_service.dart';
import '../../../data/services/translation_usage_service.dart';
import '../../../data/services/ble_manager.dart'; import '../../../data/services/ble_manager.dart';
// 切换音频输出设备的枚举 // 切换音频输出设备的枚举
@ -40,6 +40,15 @@ class TranslationController extends GetxController {
final bleManager = Get.find<BleManager>(); final bleManager = Get.find<BleManager>();
//音乐播放管理 //音乐播放管理
final _musiceManager = Get.find<MusicManager>(); final _musiceManager = Get.find<MusicManager>();
// 统计服务
late final TranslationUsageService _usageService;
// ASR统计相关 - 精确时长统计
DateTime? _asrSessionStartTime; // ASR会话开始时间
DateTime? _asrActiveStartTime; // ASR活跃开始时间(用户实际开始语音识别的时间)
Duration _asrActiveDuration = Duration.zero; // ASR累计活跃时长
bool _isAsrActive = false; // ASR是否处于活跃状态
// 音频输入源 // 音频输入源
bool _audioSourceType = false; bool _audioSourceType = false;
//是否开启播放 //是否开启播放
@ -122,11 +131,16 @@ class TranslationController extends GetxController {
// 监听识别结果 // 监听识别结果
if (activeSpeaker.value != 0) { if (activeSpeaker.value != 0) {
isPlayback = false; isPlayback = false;
final recognitionStream = await _asrService.startContinuousRecognition(_audioSourceType);
await _asrService.startContinuousRecognition(_audioSourceType);
// 开始ASR活跃时长跟踪(面对面模式)
_startAsrActiveTracking();
lastActiveSpeaker.value = id; lastActiveSpeaker.value = id;
} else { } else {
// 停止ASR活跃时长跟踪(面对面模式)
_stopAsrActiveTracking();
if (fTFTranslationResult != '' && !isPlayback) { if (fTFTranslationResult != '' && !isPlayback) {
isPlayback = true; isPlayback = true;
await _ttsService.setAudioOutputDevice(lastActiveSpeaker.value); await _ttsService.setAudioOutputDevice(lastActiveSpeaker.value);
@ -172,6 +186,9 @@ class TranslationController extends GetxController {
super.onInit(); super.onInit();
//_setupEventListeners(); //_setupEventListeners();
// 初始化统计服务
_initUsageService();
// 加载上次保存的语言设置 // 加载上次保存的语言设置
_loadSavedLanguages(); _loadSavedLanguages();
@ -198,6 +215,20 @@ class TranslationController extends GetxController {
// 参考Agent模块的实现模式 // 参考Agent模块的实现模式
} }
// 初始化统计服务
void _initUsageService() {
try {
// 确保统计服务已注册
if (!Get.isRegistered<TranslationUsageService>()) {
Get.put(TranslationUsageService(), permanent: true);
}
_usageService = Get.find<TranslationUsageService>();
Logger.info('翻译统计服务初始化完成');
} catch (e) {
Logger.error('翻译统计服务初始化失败: $e');
}
}
// 加载保存的语言设置 // 加载保存的语言设置
void _loadSavedLanguages() { void _loadSavedLanguages() {
final savedSourceLanguage = _storage.read(_sourceLanguageKey); final savedSourceLanguage = _storage.read(_sourceLanguageKey);
@ -416,6 +447,17 @@ class TranslationController extends GetxController {
} }
currentSessionId = DateTime.now().millisecondsSinceEpoch.toString(); 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") { if (currentMode.value == "audioVideo") {
bleManager.openDecoder(); bleManager.openDecoder();
@ -439,8 +481,9 @@ class TranslationController extends GetxController {
_asrService.stopContinuousRecognition(); _asrService.stopContinuousRecognition();
} else { } else {
await _ttsService.setAudioOutputDevice(0); await _ttsService.setAudioOutputDevice(0);
final recognitionStream = await _asrService.startContinuousRecognition(_audioSourceType);
await _asrService.startContinuousRecognition(_audioSourceType); // 开始ASR活跃时长计时
_startAsrActiveTracking();
} }
isRecognizing.value = true; isRecognizing.value = true;
if (isRecording.value && isRecognizing.value) { if (isRecording.value && isRecognizing.value) {
@ -481,6 +524,9 @@ class TranslationController extends GetxController {
restoreOriginalAudioState(); restoreOriginalAudioState();
await _asrService.stopContinuousRecognition(); await _asrService.stopContinuousRecognition();
// 停止ASR活跃时长计时
_stopAsrActiveTracking();
isRecognizing.value = false; isRecognizing.value = false;
// 如果当前有中间结果,将其作为最终结果处理 // 如果当前有中间结果,将其作为最终结果处理
@ -558,6 +604,8 @@ class TranslationController extends GetxController {
Future<void> handleFinalResult(String text) async { Future<void> handleFinalResult(String text) async {
if (text.isEmpty || translationHistory.isEmpty) return; if (text.isEmpty || translationHistory.isEmpty) return;
// ASR识别到最终结果,表示用户完成了一次有效的语音输入
// 更新当前项 // 更新当前项
if (translationHistory.last.isIntermediate) { if (translationHistory.last.isIntermediate) {
translationHistory.last.sourceText = text; translationHistory.last.sourceText = text;
@ -660,6 +708,33 @@ class TranslationController extends GetxController {
// 【改进】只有在最终结果时才保存,避免中间态时的频繁保存 // 【改进】只有在最终结果时才保存,避免中间态时的频繁保存
if (isFinal) { if (isFinal) {
saveTranslationHistory(); 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 final voiceName = _languageManager
.getTtsVoiceNameByAsrCode(detectedTargetLanguageCode) ?? .getTtsVoiceNameByAsrCode(detectedTargetLanguageCode) ??
'en-US-AriaNeural'; 'en-US-AriaNeural';
print("voiceName=$voiceName,text$text"); Logger.info("voiceName=$voiceName,text=$text");
final startTime = DateTime.now();
await _ttsService.setVoice(voiceName); await _ttsService.setVoice(voiceName);
await _ttsService.speakOnce(text); 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) { } catch (e) {
Logger.error('播放翻译失败: ${e.toString()}'); Logger.error('播放翻译失败: ${e.toString()}');
} }
@ -724,6 +822,8 @@ class TranslationController extends GetxController {
if (isRecognizing.value) { if (isRecognizing.value) {
await stopRecognition(); await stopRecognition();
} }
// 确保停止ASR活跃时长跟踪
_stopAsrActiveTracking();
stopRecording(); stopRecording();
_translationDebounceTimer?.cancel(); _translationDebounceTimer?.cancel();
await _ttsService.stop(); await _ttsService.stop();
@ -1210,6 +1310,54 @@ class TranslationController extends GetxController {
void scrollToBottom({bool animate = true}) { void scrollToBottom({bool animate = true}) {
_scrollToBottom(animate: animate); _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');
}
}
} }
// 翻译项模型 // 翻译项模型

258
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<TranslationUsageService>();
// 成本配置
final RxMap<String, double> costConfig = <String, double>{}.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<String, double>.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<void> 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<String> getCostSavingTips() {
final tips = <String>[];
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)}%';
}
}

25
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 'package:get_storage/get_storage.dart';
import 'dart:async'; import 'dart:async';
import '../../../core/utils/logger.dart'; import '../../../core/utils/logger.dart';
import '../../../data/services/translation_usage_service.dart';
class TranslationDetailController extends GetxController { class TranslationDetailController extends GetxController {
// 依赖注入 // 依赖注入
final VolcanoAIService _aiService = Get.find<VolcanoAIService>(); final VolcanoAIService _aiService = Get.find<VolcanoAIService>();
final GetStorage _storage = GetStorage(); final GetStorage _storage = GetStorage();
late final TranslationUsageService _usageService;
// 存储键前缀 // 存储键前缀
static const String _summaryKeyPrefix = 'translation_summary_'; static const String _summaryKeyPrefix = 'translation_summary_';
@ -33,13 +35,23 @@ class TranslationDetailController extends GetxController {
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
// 初始化统计服务
try {
if (!Get.isRegistered<TranslationUsageService>()) {
Get.put(TranslationUsageService(), permanent: true);
}
_usageService = Get.find<TranslationUsageService>();
} catch (e) {
Logger.error('初始化统计服务失败: $e');
}
// 从路由参数中获取数据 // 从路由参数中获取数据
if (Get.arguments != null) { if (Get.arguments != null) {
currentItem = Get.arguments['item']; currentItem = Get.arguments['item'];
sessionItems = Get.arguments['sessionItems']; sessionItems = Get.arguments['sessionItems'];
historyController = Get.arguments['historyController']; historyController = Get.arguments['historyController'];
// 异步加载数据,避免阻塞主线程 // 异步加载数据,避免阻塞主线程
_asyncInitialize(); _asyncInitialize();
} else { } else {
@ -170,8 +182,15 @@ class TranslationDetailController extends GetxController {
if (result.isNotEmpty) { if (result.isNotEmpty) {
summaryResult.value = result; summaryResult.value = result;
saveSummary(result); saveSummary(result);
// 记录AI摘要API调用统计
try {
_usageService.recordAiSummaryApiCall();
} catch (e) {
Logger.error('记录AI摘要统计失败: $e');
}
} }
return result; return result;
} catch (e) { } catch (e) {
final errorMessage = '生成摘要失败: ${e.toString()}'; final errorMessage = '生成摘要失败: ${e.toString()}';

358
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<TranslationUsageService>();
// 状态变量
final isLoading = false.obs;
final selectedTabIndex = 0.obs; // 0=今日, 1=本周, 2=本月, 3=历史
final selectedTimeRange = '今日'.obs;
// 统计数据
final Rx<TranslationUsageStats> todayStats =
TranslationUsageStats.empty('').obs;
final Rx<TranslationUsageStats> weekStats =
TranslationUsageStats.empty('').obs;
final Rx<TranslationUsageStats> monthStats =
TranslationUsageStats.empty('').obs;
final RxList<TranslationUsageStats> historyStats =
<TranslationUsageStats>[].obs;
// 实时统计数据
RealtimeUsageStats get realtimeStats => _usageService.realtimeStats.value;
// 成本配置
final RxMap<String, double> costConfig = <String, double>{}.obs;
final RxDouble dailyCostThreshold = 10.0.obs;
final RxDouble monthlyCostThreshold = 300.0.obs;
// 图表数据
final RxList<Map<String, dynamic>> chartData = <Map<String, dynamic>>[].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<void> 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<void> _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<void> _loadMonthStats() async {
try {
final now = DateTime.now();
monthStats.value =
await _usageService.getMonthlyStats(now.year, now.month);
} catch (e) {
Logger.error('加载本月统计失败: $e');
}
}
/// 加载历史统计数据
Future<void> _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<TranslationUsageStats> 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<String, int> combinedLanguagePairUsage = {};
final Map<String, int> 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<Map<String, dynamic>> 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<void> 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<void> 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';
}
}
}

757
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<TranslationStatsController> {
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<String>(
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,
),
),
],
);
}
}

9
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/views/translation_view.dart';
import '../modules/translation/bindings/translation_binding.dart'; import '../modules/translation/bindings/translation_binding.dart';
import '../modules/translation/views/translation_history_view.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/bindings/translation_list_binding.dart';
import '../modules/translation/views/translation_list_view.dart'; import '../modules/translation/views/translation_list_view.dart';
import '../modules/translation/bindings/translation_history_binding.dart'; import '../modules/translation/bindings/translation_history_binding.dart';
@ -148,6 +150,13 @@ abstract class AppPages {
transition: Transition.rightToLeft, transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250), transitionDuration: Duration(milliseconds: 250),
), ),
GetPage(
name: Routes.translationStats,
page: () => const TranslationStatsView(),
binding: TranslationStatsBinding(),
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage( GetPage(
name: Routes.translationList, name: Routes.translationList,
page: () => const TranslationListView(), page: () => const TranslationListView(),

1
lib/routes/app_routes.dart

@ -28,6 +28,7 @@ abstract class Routes {
// 语言功能 // 语言功能
static const translation = '/translation'; // 实时翻译 static const translation = '/translation'; // 实时翻译
static const translationHistory = '/translation/history'; // 翻译历史 static const translationHistory = '/translation/history'; // 翻译历史
static const translationStats = '/translation/stats'; // 翻译统计
static const speechDemo = '/speech_demo'; // 语音演示 static const speechDemo = '/speech_demo'; // 语音演示
static const translationList = '/translation/List'; // 翻译历史 static const translationList = '/translation/List'; // 翻译历史
static const ftftranslation = '/ftftranslation'; // 面对面翻译 static const ftftranslation = '/ftftranslation'; // 面对面翻译

Loading…
Cancel
Save