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