Browse Source
- 添加翻译统计页面及相关路由配置 - 实现翻译、ASR、TTS API调用统计功能 - 新增成本计算和预警机制 - 完善统计数据的存储和展示逻辑newdev_shunjiawei
11 changed files with 2570 additions and 10 deletions
@ -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; |
|||
} |
|||
@ -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()); |
|||
} |
|||
} |
|||
} |
|||
@ -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(), |
|||
); |
|||
} |
|||
} |
|||
@ -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)}%'; |
|||
} |
|||
} |
|||
@ -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'; |
|||
} |
|||
} |
|||
} |
|||
@ -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, |
|||
), |
|||
), |
|||
], |
|||
); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue