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.
328 lines
9.3 KiB
328 lines
9.3 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';
|
|
import 'usage_controller.dart';
|
|
|
|
class TranslationStatsController extends GetxController {
|
|
final UsageStatsService _usageService = Get.find<UsageStatsService>();
|
|
|
|
final isLoading = false.obs;
|
|
final selectedTabIndex = 0.obs;
|
|
final selectedTimeRange = 'today'.tr.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.realtimeTranslationStats.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.todayTranslationStats, (stats) {
|
|
todayStats.value = stats;
|
|
_updateChartData();
|
|
});
|
|
|
|
loadAllStats();
|
|
}
|
|
|
|
Future<void> loadAllStats() async {
|
|
isLoading.value = true;
|
|
|
|
try {
|
|
todayStats.value = _usageService.todayTranslationStats.value;
|
|
|
|
await _loadWeekStats();
|
|
|
|
await _loadMonthStats();
|
|
|
|
await _loadHistoryStats();
|
|
|
|
_updateChartData();
|
|
|
|
Logger.info('统计数据加载完成');
|
|
} catch (e) {
|
|
Logger.error('加载统计数据失败: $e');
|
|
Get.snackbar(
|
|
'loadFailed'.tr, // 加载失败
|
|
'statsLoadFailedTryLater'.tr, // 统计数据加载失败,请稍后重试
|
|
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;
|
|
|
|
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;
|
|
|
|
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: 0.0,
|
|
);
|
|
}
|
|
|
|
void _updateChartData() {
|
|
final List<Map<String, dynamic>> data = [];
|
|
|
|
switch (selectedTabIndex.value) {
|
|
case 0:
|
|
data.add({
|
|
'label': 'today'.tr, // 今日
|
|
'apiCalls': todayStats.value.totalApiCalls,
|
|
'cost': 0.0,
|
|
'characters': todayStats.value.totalCharacterCount,
|
|
});
|
|
break;
|
|
case 1:
|
|
data.add({
|
|
'label': 'thisWeek'.tr, // 本周
|
|
'apiCalls': weekStats.value.totalApiCalls,
|
|
'cost': 0.0,
|
|
'characters': weekStats.value.totalCharacterCount,
|
|
});
|
|
break;
|
|
case 2:
|
|
data.add({
|
|
'label': 'thisMonth'.tr, // 本月
|
|
'apiCalls': monthStats.value.totalApiCalls,
|
|
'cost': 0.0,
|
|
'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': 0.0,
|
|
'characters': stats.totalCharacterCount,
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
|
|
chartData.value = data;
|
|
}
|
|
|
|
void switchTimeRange(int index) {
|
|
selectedTabIndex.value = index;
|
|
|
|
switch (index) {
|
|
case 0:
|
|
selectedTimeRange.value = 'today'.tr; // 今日
|
|
break;
|
|
case 1:
|
|
selectedTimeRange.value = 'thisWeek'.tr; // 本周
|
|
break;
|
|
case 2:
|
|
selectedTimeRange.value = 'thisMonth'.tr; // 本月
|
|
break;
|
|
case 3:
|
|
selectedTimeRange.value = 'history'.tr; // 历史
|
|
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(
|
|
'cleanupComplete'.tr, // 清理完成
|
|
'cleanedOldData'.tr, // 已清理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(
|
|
'exportFeature'.tr, // 导出功能
|
|
'exportInDevelopment'.tr, // 数据导出功能正在开发中
|
|
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${'seconds'.tr}'; // 秒
|
|
} else if (seconds < 3600) {
|
|
final minutes = seconds ~/ 60;
|
|
final remainingSeconds = seconds % 60;
|
|
return '$minutes${'minutes'.tr}$remainingSeconds${'seconds'.tr}'; // 分钟秒
|
|
} else {
|
|
final hours = seconds ~/ 3600;
|
|
final minutes = (seconds % 3600) ~/ 60;
|
|
return '$hours${'hours'.tr}$minutes${'minutes'.tr}'; // 小时分钟
|
|
}
|
|
}
|
|
|
|
String formatCost(double cost) {
|
|
return '¥${cost.toStringAsFixed(2)}';
|
|
}
|
|
|
|
int _titleTapCount = 0;
|
|
int _lastTitleTapTs = 0;
|
|
|
|
void onTitleTap() {
|
|
final now = DateTime.now().millisecondsSinceEpoch;
|
|
if (now - _lastTitleTapTs <= 800) {
|
|
_titleTapCount++;
|
|
} else {
|
|
_titleTapCount = 1;
|
|
}
|
|
_lastTitleTapTs = now;
|
|
if (_titleTapCount >= 3) {
|
|
_titleTapCount = 0;
|
|
final uc = Get.find<UsageController>();
|
|
uc.toggleAiChat(!uc.showAiChat.value);
|
|
}
|
|
}
|
|
|
|
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';
|
|
}
|
|
}
|
|
}
|
|
|