You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
322 lines
8.9 KiB
322 lines
8.9 KiB
import 'package:get/get.dart';
|
|
import '../../../data/services/usage_stats_service.dart';
|
|
import '../../../data/models/translation_usage_stats.dart';
|
|
import '../../../core/utils/logger.dart';
|
|
|
|
class TranslationStatsController extends GetxController {
|
|
final UsageStatsService _usageService = Get.find<UsageStatsService>();
|
|
|
|
final isLoading = false.obs;
|
|
final selectedTabIndex = 0.obs;
|
|
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();
|
|
|
|
await _loadHistoryStats();
|
|
|
|
_updateChartData();
|
|
|
|
Logger.info('统计数据加载完成');
|
|
} catch (e) {
|
|
Logger.error('加载统计数据失败: $e');
|
|
Get.snackbar(
|
|
'加载失败',
|
|
'统计数据加载失败,请稍后重试',
|
|
snackPosition: SnackPosition.TOP,
|
|
backgroundColor: Get.theme.colorScheme.error.withValues(alpha: 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;
|
|
|
|
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: '${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.withValues(alpha: 0.1),
|
|
colorText: Get.theme.colorScheme.primary,
|
|
);
|
|
}
|
|
|
|
Future<void> exportStats() async {
|
|
try {
|
|
Get.snackbar(
|
|
'导出功能',
|
|
'数据导出功能正在开发中',
|
|
snackPosition: SnackPosition.TOP,
|
|
backgroundColor: Get.theme.colorScheme.primary.withValues(alpha: 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';
|
|
}
|
|
}
|
|
}
|
|
|