Browse Source

Merge branch 'new_dev' of https://github.com/deepcloud2048/deep_voice into new_dev

weicu
tanlongsheng 1 year ago
parent
commit
a590010dc0
  1. 1
      .gitignore
  2. 15
      lib/core/translations/language/en_us.dart
  3. 7
      lib/core/translations/language/zh_cn.dart
  4. 450
      lib/data/models/translation_usage_stats.dart
  5. 519
      lib/data/services/translation_usage_service.dart
  6. 42
      lib/modules/agent/controllers/agent_controller.dart
  7. 2
      lib/modules/agent/views/agent_view.dart
  8. 77
      lib/modules/agent/views/message_bubble.dart
  9. 5
      lib/modules/explore/controllers/explore_controller.dart
  10. 72
      lib/modules/explore/views/explore_view.dart
  11. 257
      lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart
  12. 33
      lib/modules/pairing/controllers/pairing_controller.dart
  13. 342
      lib/modules/pairing/views/pairing_view.dart
  14. 7
      lib/modules/pairing/views/pairing_view_new.dart
  15. 9
      lib/modules/profile/controllers/profile_controller.dart
  16. 68
      lib/modules/profile/views/profile_meeting_view.dart
  17. 31
      lib/modules/profile/views/profile_view.dart
  18. 2
      lib/modules/settings/views/settings_view.dart
  19. 18
      lib/modules/translation/bindings/translation_stats_binding.dart
  20. 162
      lib/modules/translation/controllers/translation_controller.dart
  21. 258
      lib/modules/translation/controllers/translation_cost_controller.dart
  22. 19
      lib/modules/translation/controllers/translation_detail_controller.dart
  23. 358
      lib/modules/translation/controllers/translation_stats_controller.dart
  24. 2
      lib/modules/translation/views/translation_detail_view.dart
  25. 757
      lib/modules/translation/views/translation_stats_view.dart
  26. 168
      lib/modules/translation/views/translation_view.dart
  27. 9
      lib/routes/app_pages.dart
  28. 1
      lib/routes/app_routes.dart
  29. 64
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  30. 16
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt
  31. 3
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  32. 15
      local_plugins/agent_service/lib/agent_service.dart
  33. 2
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  34. 54
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt

1
.gitignore

@ -221,3 +221,4 @@ fastlane/test_output
fastlane/readme.md
.trae/rules/project_rules.md
.augment/rules/imported/project_rules.md
.kiro/steering/flutter_project_standards.md

15
lib/core/translations/language/en_us.dart

@ -805,7 +805,8 @@ const Map<String, String> enUS = {
'invalidDeviceAddress': 'Invalid device address',
'bleDeviceAssociationFailed': 'BLE device association failed',
'deviceAssociationFailedRetry': 'Device association failed, please retry',
'bleConnectionTimeoutRetry': 'BLE connection timeout, please retry',
'bleConnectionTimeoutRetry':
'BLE connection timeout. Please restart the earphone and try again',
'blePairingStartupFailed': 'BLE pairing startup failed',
'bleConnectionFailed': 'BLE connection failed',
'deviceAuthorizationFailedCheckCode':
@ -892,4 +893,16 @@ const Map<String, String> enUS = {
"乌尔都语": "Urdu",
"波斯语": "Persian",
"斯瓦希里语": "Swahili",
// Time related
"justNow": "Just now",
"minutesAgo": "@minutes minutes ago",
"yesterday": "Yesterday",
// "monday": "Monday",
// "tuesday": "Tuesday",
// "wednesday": "Wednesday",
// "thursday": "Thursday",
// "friday": "Friday",
// "saturday": "Saturday",
// "sunday": "Sunday",
};

7
lib/core/translations/language/zh_cn.dart

@ -741,7 +741,7 @@ const Map<String, String> zhCN = {
'invalidDeviceAddress': '设备地址无效',
'bleDeviceAssociationFailed': 'BLE设备关联失败',
'deviceAssociationFailedRetry': '设备关联失败,请重试',
'bleConnectionTimeoutRetry': 'BLE连接超时,请重试',
'bleConnectionTimeoutRetry': 'BLE连接超时,请重启耳机后再试',
'blePairingStartupFailed': 'BLE配对启动失败',
'bleConnectionFailed': 'BLE连接失败',
'deviceAuthorizationFailedCheckCode': '设备授权失败,请检查授权码',
@ -824,4 +824,9 @@ const Map<String, String> zhCN = {
"乌尔都语": "乌尔都语",
"波斯语": "波斯语",
"斯瓦希里语": "斯瓦希里语",
// 时间相关
"justNow": "刚刚",
"minutesAgo": "@minutes分钟前",
"yesterday": "昨天",
};

450
lib/data/models/translation_usage_stats.dart

@ -0,0 +1,450 @@
/// 翻译模式统计数据
class TranslationModeStats {
final int apiCalls; // API调用次数
final int sourceCharacters; // 源文本字符数
final int effectiveAudioDuration; // 有效音频时长
final int ttsCharacters; // TTS字符数
final double cost; // 成本
TranslationModeStats({
this.apiCalls = 0,
this.sourceCharacters = 0,
this.effectiveAudioDuration = 0,
this.ttsCharacters = 0,
this.cost = 0.0,
});
factory TranslationModeStats.fromJson(Map<String, dynamic> json) {
return TranslationModeStats(
apiCalls: json['apiCalls'] ?? 0,
sourceCharacters: json['sourceCharacters'] ?? 0,
effectiveAudioDuration: json['effectiveAudioDuration'] ?? 0,
ttsCharacters: json['ttsCharacters'] ?? 0,
cost: (json['cost'] ?? 0.0).toDouble(),
);
}
Map<String, dynamic> toJson() {
return {
'apiCalls': apiCalls,
'sourceCharacters': sourceCharacters,
'effectiveAudioDuration': effectiveAudioDuration,
'ttsCharacters': ttsCharacters,
'cost': cost,
};
}
TranslationModeStats copyWith({
int? apiCalls,
int? sourceCharacters,
int? effectiveAudioDuration,
int? ttsCharacters,
double? cost,
}) {
return TranslationModeStats(
apiCalls: apiCalls ?? this.apiCalls,
sourceCharacters: sourceCharacters ?? this.sourceCharacters,
effectiveAudioDuration: effectiveAudioDuration ?? this.effectiveAudioDuration,
ttsCharacters: ttsCharacters ?? this.ttsCharacters,
cost: cost ?? this.cost,
);
}
}
/// 语言对统计数据
class LanguagePairStats {
final String languagePair; // 语言对,如"zh-CN→en-US"
final int translationCount; // 翻译次数
final int sourceCharacters; // 源文本字符数
final double cost; // 成本
LanguagePairStats({
required this.languagePair,
this.translationCount = 0,
this.sourceCharacters = 0,
this.cost = 0.0,
});
factory LanguagePairStats.fromJson(Map<String, dynamic> json) {
return LanguagePairStats(
languagePair: json['languagePair'] ?? '',
translationCount: json['translationCount'] ?? 0,
sourceCharacters: json['sourceCharacters'] ?? 0,
cost: (json['cost'] ?? 0.0).toDouble(),
);
}
Map<String, dynamic> toJson() {
return {
'languagePair': languagePair,
'translationCount': translationCount,
'sourceCharacters': sourceCharacters,
'cost': cost,
};
}
}
/// 小时统计数据
class HourlyStats {
final int hour; // 小时(0-23)
final int apiCalls; // API调用次数
final int sourceCharacters; // 源文本字符数
final int effectiveAudioDuration; // 有效音频时长
final double cost; // 成本
HourlyStats({
required this.hour,
this.apiCalls = 0,
this.sourceCharacters = 0,
this.effectiveAudioDuration = 0,
this.cost = 0.0,
});
factory HourlyStats.fromJson(Map<String, dynamic> json) {
return HourlyStats(
hour: json['hour'] ?? 0,
apiCalls: json['apiCalls'] ?? 0,
sourceCharacters: json['sourceCharacters'] ?? 0,
effectiveAudioDuration: json['effectiveAudioDuration'] ?? 0,
cost: (json['cost'] ?? 0.0).toDouble(),
);
}
Map<String, dynamic> toJson() {
return {
'hour': hour,
'apiCalls': apiCalls,
'sourceCharacters': sourceCharacters,
'effectiveAudioDuration': effectiveAudioDuration,
'cost': cost,
};
}
}
/// 翻译使用统计数据模型 - 优化版
class TranslationUsageStats {
// 基础信息
final String id; // 统计记录ID
final DateTime date; // 统计日期
final String userId; // 用户ID
final String timeGranularity; // 时间粒度:realtime/hour/day/week/month
// API调用统计 - 精确化
final int translationApiCalls; // 翻译API调用次数
final int asrApiCalls; // ASR语音识别调用次数
final int ttsApiCalls; // TTS语音合成调用次数
final int aiSummaryApiCalls; // AI摘要生成调用次数(已废弃但保留兼容性)
// 精确计费参数统计
final int sourceTextCharacters; // 源文本字符数(翻译API计费基础)
final int effectiveAudioDuration; // ASR有效语音时长(秒,ASR计费基础)
final int ttsTextCharacters; // TTS文本字符数(TTS计费基础)
final int totalSessionDuration; // 总会话时长(秒,用于分析)
// 网络流量统计
final int asrNetworkBytes; // ASR网络流量(字节)
final int ttsNetworkBytes; // TTS网络流量(字节)
final int translationNetworkBytes; // 翻译API网络流量(字节)
// 多维度分类统计
final int sessionCount; // 翻译会话数量
final Map<String, TranslationModeStats> modeStats; // 按翻译模式分类统计
final Map<String, LanguagePairStats> languagePairStats; // 按语言对分类统计
final Map<String, HourlyStats> hourlyStats; // 按小时分类统计(仅日统计包含)
// 成本相关
final double estimatedCost; // 预估成本
final String currency; // 货币单位
final Map<String, double> costBreakdown; // 成本分解(ASR/TTS/Translation)
TranslationUsageStats({
required this.id,
required this.date,
required this.userId,
this.timeGranularity = 'day',
this.translationApiCalls = 0,
this.asrApiCalls = 0,
this.ttsApiCalls = 0,
this.aiSummaryApiCalls = 0,
this.sourceTextCharacters = 0,
this.effectiveAudioDuration = 0,
this.ttsTextCharacters = 0,
this.totalSessionDuration = 0,
this.asrNetworkBytes = 0,
this.ttsNetworkBytes = 0,
this.translationNetworkBytes = 0,
this.sessionCount = 0,
this.modeStats = const {},
this.languagePairStats = const {},
this.hourlyStats = const {},
this.estimatedCost = 0.0,
this.currency = 'CNY',
this.costBreakdown = const {},
});
factory TranslationUsageStats.fromJson(Map<String, dynamic> json) {
// 处理多维度统计数据的反序列化
final modeStatsMap = <String, TranslationModeStats>{};
if (json['modeStats'] != null) {
final modeStatsJson = json['modeStats'] as Map<String, dynamic>;
modeStatsJson.forEach((key, value) {
modeStatsMap[key] = TranslationModeStats.fromJson(value);
});
}
final languagePairStatsMap = <String, LanguagePairStats>{};
if (json['languagePairStats'] != null) {
final languagePairStatsJson = json['languagePairStats'] as Map<String, dynamic>;
languagePairStatsJson.forEach((key, value) {
languagePairStatsMap[key] = LanguagePairStats.fromJson(value);
});
}
final hourlyStatsMap = <String, HourlyStats>{};
if (json['hourlyStats'] != null) {
final hourlyStatsJson = json['hourlyStats'] as Map<String, dynamic>;
hourlyStatsJson.forEach((key, value) {
hourlyStatsMap[key] = HourlyStats.fromJson(value);
});
}
return TranslationUsageStats(
id: json['id'] ?? '',
date: DateTime.parse(json['date']),
userId: json['userId'] ?? '',
timeGranularity: json['timeGranularity'] ?? 'day',
translationApiCalls: json['translationApiCalls'] ?? 0,
asrApiCalls: json['asrApiCalls'] ?? 0,
ttsApiCalls: json['ttsApiCalls'] ?? 0,
aiSummaryApiCalls: json['aiSummaryApiCalls'] ?? 0,
sourceTextCharacters: json['sourceTextCharacters'] ?? json['sourceCharacterCount'] ?? 0, // 兼容旧字段
effectiveAudioDuration: json['effectiveAudioDuration'] ?? json['audioRecordingDuration'] ?? 0, // 兼容旧字段
ttsTextCharacters: json['ttsTextCharacters'] ?? json['targetCharacterCount'] ?? 0, // 兼容旧字段
totalSessionDuration: json['totalSessionDuration'] ?? 0,
asrNetworkBytes: json['asrNetworkBytes'] ?? 0,
ttsNetworkBytes: json['ttsNetworkBytes'] ?? 0,
translationNetworkBytes: json['translationNetworkBytes'] ?? 0,
sessionCount: json['sessionCount'] ?? 0,
modeStats: modeStatsMap,
languagePairStats: languagePairStatsMap,
hourlyStats: hourlyStatsMap,
estimatedCost: (json['estimatedCost'] ?? 0.0).toDouble(),
currency: json['currency'] ?? 'CNY',
costBreakdown: Map<String, double>.from(json['costBreakdown'] ?? {}),
);
}
Map<String, dynamic> toJson() {
// 序列化多维度统计数据
final modeStatsJson = <String, dynamic>{};
modeStats.forEach((key, value) {
modeStatsJson[key] = value.toJson();
});
final languagePairStatsJson = <String, dynamic>{};
languagePairStats.forEach((key, value) {
languagePairStatsJson[key] = value.toJson();
});
final hourlyStatsJson = <String, dynamic>{};
hourlyStats.forEach((key, value) {
hourlyStatsJson[key] = value.toJson();
});
return {
'id': id,
'date': date.toIso8601String(),
'userId': userId,
'timeGranularity': timeGranularity,
'translationApiCalls': translationApiCalls,
'asrApiCalls': asrApiCalls,
'ttsApiCalls': ttsApiCalls,
'aiSummaryApiCalls': aiSummaryApiCalls,
'sourceTextCharacters': sourceTextCharacters,
'effectiveAudioDuration': effectiveAudioDuration,
'ttsTextCharacters': ttsTextCharacters,
'totalSessionDuration': totalSessionDuration,
'asrNetworkBytes': asrNetworkBytes,
'ttsNetworkBytes': ttsNetworkBytes,
'translationNetworkBytes': translationNetworkBytes,
'sessionCount': sessionCount,
'modeStats': modeStatsJson,
'languagePairStats': languagePairStatsJson,
'hourlyStats': hourlyStatsJson,
'estimatedCost': estimatedCost,
'currency': currency,
'costBreakdown': costBreakdown,
// 保持向后兼容性
'sourceCharacterCount': sourceTextCharacters,
'targetCharacterCount': ttsTextCharacters,
'audioRecordingDuration': effectiveAudioDuration,
'ttsPlaybackDuration': totalSessionDuration,
};
}
/// 创建空的统计记录
factory TranslationUsageStats.empty(String userId) {
return TranslationUsageStats(
id: DateTime.now().millisecondsSinceEpoch.toString(),
date: DateTime.now(),
userId: userId,
);
}
/// 复制并更新统计数据
TranslationUsageStats copyWith({
String? id,
DateTime? date,
String? userId,
String? timeGranularity,
int? translationApiCalls,
int? asrApiCalls,
int? ttsApiCalls,
int? aiSummaryApiCalls,
int? sourceTextCharacters,
int? effectiveAudioDuration,
int? ttsTextCharacters,
int? totalSessionDuration,
int? asrNetworkBytes,
int? ttsNetworkBytes,
int? translationNetworkBytes,
int? sessionCount,
Map<String, TranslationModeStats>? modeStats,
Map<String, LanguagePairStats>? languagePairStats,
Map<String, HourlyStats>? hourlyStats,
double? estimatedCost,
String? currency,
Map<String, double>? costBreakdown,
}) {
return TranslationUsageStats(
id: id ?? this.id,
date: date ?? this.date,
userId: userId ?? this.userId,
timeGranularity: timeGranularity ?? this.timeGranularity,
translationApiCalls: translationApiCalls ?? this.translationApiCalls,
asrApiCalls: asrApiCalls ?? this.asrApiCalls,
ttsApiCalls: ttsApiCalls ?? this.ttsApiCalls,
aiSummaryApiCalls: aiSummaryApiCalls ?? this.aiSummaryApiCalls,
sourceTextCharacters: sourceTextCharacters ?? this.sourceTextCharacters,
effectiveAudioDuration: effectiveAudioDuration ?? this.effectiveAudioDuration,
ttsTextCharacters: ttsTextCharacters ?? this.ttsTextCharacters,
totalSessionDuration: totalSessionDuration ?? this.totalSessionDuration,
asrNetworkBytes: asrNetworkBytes ?? this.asrNetworkBytes,
ttsNetworkBytes: ttsNetworkBytes ?? this.ttsNetworkBytes,
translationNetworkBytes: translationNetworkBytes ?? this.translationNetworkBytes,
sessionCount: sessionCount ?? this.sessionCount,
modeStats: modeStats ?? this.modeStats,
languagePairStats: languagePairStats ?? this.languagePairStats,
hourlyStats: hourlyStats ?? this.hourlyStats,
estimatedCost: estimatedCost ?? this.estimatedCost,
currency: currency ?? this.currency,
costBreakdown: costBreakdown ?? this.costBreakdown,
);
}
/// 获取总API调用次数
int get totalApiCalls => translationApiCalls + asrApiCalls + ttsApiCalls + aiSummaryApiCalls;
/// 获取总字符数(精确计费基础)
int get totalCharacterCount => sourceTextCharacters + ttsTextCharacters;
/// 获取总音频时长(包含有效时长和总会话时长)
int get totalAudioDuration => effectiveAudioDuration + totalSessionDuration;
/// 获取总网络流量
int get totalNetworkBytes => asrNetworkBytes + ttsNetworkBytes + translationNetworkBytes;
/// 获取最常用的语言对
String get mostUsedLanguagePair {
if (languagePairStats.isEmpty) return '无数据';
return languagePairStats.entries
.reduce((a, b) => a.value.translationCount > b.value.translationCount ? a : b)
.key;
}
/// 获取最常用的翻译模式
String get mostUsedMode {
if (modeStats.isEmpty) return '无数据';
return modeStats.entries
.reduce((a, b) => a.value.apiCalls > b.value.apiCalls ? a : b)
.key;
}
// 向后兼容性getter
/// @deprecated 使用 sourceTextCharacters 替代
int get sourceCharacterCount => sourceTextCharacters;
/// @deprecated 使用 ttsTextCharacters 替代
int get targetCharacterCount => ttsTextCharacters;
/// @deprecated 使用 effectiveAudioDuration 替代
int get audioRecordingDuration => effectiveAudioDuration;
/// @deprecated 使用 totalSessionDuration 替代
int get ttsPlaybackDuration => totalSessionDuration;
/// @deprecated 使用 languagePairStats 替代
Map<String, int> get languagePairUsage {
final result = <String, int>{};
languagePairStats.forEach((key, value) {
result[key] = value.translationCount;
});
return result;
}
/// @deprecated 使用 modeStats 替代
Map<String, int> get modeUsage {
final result = <String, int>{};
modeStats.forEach((key, value) {
result[key] = value.apiCalls;
});
return result;
}
}
/// 实时统计数据模型(用于当前会话)- 优化版
class RealtimeUsageStats {
// 精确计费参数
int translationCount = 0; // 当前会话翻译次数
int sourceTextCharacters = 0; // 当前会话源文本字符数(翻译计费基础)
int ttsTextCharacters = 0; // 当前会话TTS文本字符数(TTS计费基础)
int effectiveAudioDuration = 0; // 当前会话有效语音时长(ASR计费基础)
int totalSessionDuration = 0; // 当前会话总时长
// 会话信息
String currentLanguagePair = ''; // 当前语言对
String currentMode = ''; // 当前翻译模式
DateTime sessionStartTime = DateTime.now(); // 会话开始时间
/// 重置统计数据
void reset() {
translationCount = 0;
sourceTextCharacters = 0;
ttsTextCharacters = 0;
effectiveAudioDuration = 0;
totalSessionDuration = 0;
currentLanguagePair = '';
currentMode = '';
sessionStartTime = DateTime.now();
}
/// 获取会话持续时间(分钟)
int get sessionDurationMinutes {
return DateTime.now().difference(sessionStartTime).inMinutes;
}
// 向后兼容性getter
/// @deprecated 使用 sourceTextCharacters 替代
int get sourceCharacters => sourceTextCharacters;
/// @deprecated 使用 ttsTextCharacters 替代
int get targetCharacters => ttsTextCharacters;
/// @deprecated 使用 effectiveAudioDuration 替代
int get recordingDuration => effectiveAudioDuration;
/// @deprecated 使用 totalSessionDuration 替代
int get ttsPlayDuration => totalSessionDuration;
}

519
lib/data/services/translation_usage_service.dart

@ -0,0 +1,519 @@
import 'dart:convert';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import '../models/translation_usage_stats.dart';
import '../../core/utils/logger.dart';
/// 翻译使用量统计服务
class TranslationUsageService extends GetxService {
static const String _storageKey = 'translation_usage_stats';
static const String _dailyStatsKey = 'daily_translation_stats';
static const String _costConfigKey = 'translation_cost_config';
final GetStorage _storage = GetStorage();
// 实时统计数据
final Rx<RealtimeUsageStats> realtimeStats = RealtimeUsageStats().obs;
// 当日统计数据
final Rx<TranslationUsageStats> todayStats = TranslationUsageStats.empty('').obs;
// 成本配置
final RxMap<String, double> costConfig = <String, double>{}.obs;
// 预警阈值
final RxDouble dailyCostThreshold = 10.0.obs; // 日成本预警阈值
final RxDouble monthlyCostThreshold = 300.0.obs; // 月成本预警阈值
@override
void onInit() {
super.onInit();
_initializeService();
}
/// 初始化服务
void _initializeService() {
_loadCostConfig();
_loadTodayStats();
Logger.info('翻译使用量统计服务初始化完成');
}
/// 加载成本配置
void _loadCostConfig() {
try {
final configData = _storage.read(_costConfigKey);
if (configData != null) {
costConfig.value = Map<String, double>.from(configData);
} else {
// 设置默认成本配置(每千字符的成本,单位:元)
costConfig.value = {
'translation_per_1k_chars': 0.05, // 翻译:每千字符0.05元
'asr_per_minute': 0.02, // ASR:每分钟0.02元
'tts_per_1k_chars': 0.03, // TTS:每千字符0.03元
'ai_summary_per_request': 0.01, // AI摘要:每次请求0.01元
};
_saveCostConfig();
}
} catch (e) {
Logger.error('加载成本配置失败: $e');
}
}
/// 保存成本配置
void _saveCostConfig() {
try {
_storage.write(_costConfigKey, costConfig.value);
} catch (e) {
Logger.error('保存成本配置失败: $e');
}
}
/// 加载当日统计数据
void _loadTodayStats() {
try {
final today = DateTime.now();
final todayKey = '${today.year}-${today.month.toString().padLeft(2, '0')}-${today.day.toString().padLeft(2, '0')}';
final statsData = _storage.read('${_dailyStatsKey}_$todayKey');
if (statsData != null) {
todayStats.value = TranslationUsageStats.fromJson(Map<String, dynamic>.from(statsData));
} else {
todayStats.value = TranslationUsageStats.empty(getCurrentUserId());
}
} catch (e) {
Logger.error('加载当日统计数据失败: $e');
todayStats.value = TranslationUsageStats.empty(getCurrentUserId());
}
}
/// 获取当前用户ID
String getCurrentUserId() {
try {
final userInfo = _storage.read('user_info');
if (userInfo != null && userInfo['user'] != null) {
return userInfo['user']['uid'] ?? 'anonymous';
}
} catch (e) {
Logger.warning('获取用户ID失败: $e');
}
return 'anonymous';
}
/// 记录翻译API调用
void recordTranslationApiCall({
required String sourceText,
required String targetText,
required String sourceLanguage,
required String targetLanguage,
required String mode,
}) {
try {
// 更新实时统计
realtimeStats.value.translationCount++;
realtimeStats.value.sourceTextCharacters += sourceText.length;
realtimeStats.value.ttsTextCharacters += targetText.length;
realtimeStats.value.currentLanguagePair = '$sourceLanguage-$targetLanguage';
realtimeStats.value.currentMode = mode;
realtimeStats.refresh();
// 更新当日统计 - 使用多维度统计
final languagePair = '$sourceLanguage→$targetLanguage';
// 更新语言对统计
final newLanguagePairStats = Map<String, LanguagePairStats>.from(todayStats.value.languagePairStats);
if (newLanguagePairStats.containsKey(languagePair)) {
final existing = newLanguagePairStats[languagePair]!;
newLanguagePairStats[languagePair] = LanguagePairStats(
languagePair: languagePair,
translationCount: existing.translationCount + 1,
sourceCharacters: existing.sourceCharacters + sourceText.length,
cost: existing.cost + _calculateTranslationCost(sourceText.length),
);
} else {
newLanguagePairStats[languagePair] = LanguagePairStats(
languagePair: languagePair,
translationCount: 1,
sourceCharacters: sourceText.length,
cost: _calculateTranslationCost(sourceText.length),
);
}
// 更新模式统计
final newModeStats = Map<String, TranslationModeStats>.from(todayStats.value.modeStats);
if (newModeStats.containsKey(mode)) {
final existing = newModeStats[mode]!;
newModeStats[mode] = existing.copyWith(
apiCalls: existing.apiCalls + 1,
sourceCharacters: existing.sourceCharacters + sourceText.length,
ttsCharacters: existing.ttsCharacters + targetText.length,
cost: existing.cost + _calculateTranslationCost(sourceText.length),
);
} else {
newModeStats[mode] = TranslationModeStats(
apiCalls: 1,
sourceCharacters: sourceText.length,
ttsCharacters: targetText.length,
cost: _calculateTranslationCost(sourceText.length),
);
}
todayStats.value = todayStats.value.copyWith(
translationApiCalls: todayStats.value.translationApiCalls + 1,
sourceTextCharacters: todayStats.value.sourceTextCharacters + sourceText.length,
ttsTextCharacters: todayStats.value.ttsTextCharacters + targetText.length,
languagePairStats: newLanguagePairStats,
modeStats: newModeStats,
estimatedCost: _calculateEstimatedCost(todayStats.value.copyWith(
translationApiCalls: todayStats.value.translationApiCalls + 1,
sourceTextCharacters: todayStats.value.sourceTextCharacters + sourceText.length,
ttsTextCharacters: todayStats.value.ttsTextCharacters + targetText.length,
)),
);
_saveTodayStats();
_checkCostThreshold();
Logger.info('记录翻译API调用: 源文本${sourceText.length}字符, 目标文本${targetText.length}字符');
} catch (e) {
Logger.error('记录翻译API调用失败: $e');
}
}
/// 记录ASR API调用 - 优化版(精确计费参数)
void recordAsrApiCall({
required int effectiveDurationSeconds, // 有效语音时长(VAD检测的实际语音)
required int totalDurationSeconds, // 总会话时长(包含静音)
required String language,
required String mode, // 翻译模式
String? audioFormat, // 音频格式(可选)
int? sampleRate, // 采样率(可选)
}) {
try {
// 更新实时统计(使用有效语音时长作为主要计费依据)
realtimeStats.value.effectiveAudioDuration += effectiveDurationSeconds;
realtimeStats.value.totalSessionDuration += totalDurationSeconds;
realtimeStats.refresh();
// 更新模式统计
final newModeStats = Map<String, TranslationModeStats>.from(todayStats.value.modeStats);
if (newModeStats.containsKey(mode)) {
final existing = newModeStats[mode]!;
newModeStats[mode] = existing.copyWith(
effectiveAudioDuration: existing.effectiveAudioDuration + effectiveDurationSeconds,
cost: existing.cost + _calculateAsrCost(effectiveDurationSeconds),
);
} else {
newModeStats[mode] = TranslationModeStats(
effectiveAudioDuration: effectiveDurationSeconds,
cost: _calculateAsrCost(effectiveDurationSeconds),
);
}
// 更新当日统计
todayStats.value = todayStats.value.copyWith(
asrApiCalls: todayStats.value.asrApiCalls + 1,
effectiveAudioDuration: todayStats.value.effectiveAudioDuration + effectiveDurationSeconds,
totalSessionDuration: todayStats.value.totalSessionDuration + totalDurationSeconds,
modeStats: newModeStats,
estimatedCost: _calculateEstimatedCost(todayStats.value.copyWith(
asrApiCalls: todayStats.value.asrApiCalls + 1,
effectiveAudioDuration: todayStats.value.effectiveAudioDuration + effectiveDurationSeconds,
)),
);
_saveTodayStats();
Logger.info('ASR统计: 有效语音${effectiveDurationSeconds}秒, 总时长${totalDurationSeconds}秒, 语言$language, 模式$mode');
} catch (e) {
Logger.error('记录ASR API调用失败: $e');
}
}
/// 记录TTS API调用
void recordTtsApiCall({
required String text,
required int durationSeconds,
required String language,
}) {
try {
// 更新实时统计
realtimeStats.value.totalSessionDuration += durationSeconds;
realtimeStats.refresh();
// 更新当日统计(TTS按字符数计费,不是按时长)
todayStats.value = todayStats.value.copyWith(
ttsApiCalls: todayStats.value.ttsApiCalls + 1,
ttsTextCharacters: todayStats.value.ttsTextCharacters + text.length,
totalSessionDuration: todayStats.value.totalSessionDuration + durationSeconds,
estimatedCost: _calculateEstimatedCost(todayStats.value.copyWith(
ttsApiCalls: todayStats.value.ttsApiCalls + 1,
ttsTextCharacters: todayStats.value.ttsTextCharacters + text.length,
)),
);
_saveTodayStats();
Logger.info('记录TTS API调用: 文本${text.length}字符, 时长${durationSeconds}秒');
} catch (e) {
Logger.error('记录TTS API调用失败: $e');
}
}
/// 记录AI摘要API调用
void recordAiSummaryApiCall() {
try {
// 更新当日统计
todayStats.value = todayStats.value.copyWith(
aiSummaryApiCalls: todayStats.value.aiSummaryApiCalls + 1,
estimatedCost: _calculateEstimatedCost(todayStats.value.copyWith(
aiSummaryApiCalls: todayStats.value.aiSummaryApiCalls + 1,
)),
);
_saveTodayStats();
Logger.info('记录AI摘要API调用');
} catch (e) {
Logger.error('记录AI摘要API调用失败: $e');
}
}
/// 开始新会话
void startNewSession() {
realtimeStats.value.reset();
// 更新当日会话数
todayStats.value = todayStats.value.copyWith(
sessionCount: todayStats.value.sessionCount + 1,
);
_saveTodayStats();
Logger.info('开始新的翻译会话');
}
/// 计算预估成本 - 优化版(基于精确计费参数)
double _calculateEstimatedCost(TranslationUsageStats stats) {
double cost = 0.0;
// 翻译成本:基于源文本字符数
cost += (stats.sourceTextCharacters / 1000) * (costConfig['translation_per_1k_chars'] ?? 0.05);
// ASR成本:基于有效音频时长
cost += (stats.effectiveAudioDuration / 60) * (costConfig['asr_per_minute'] ?? 0.02);
// TTS成本:基于TTS文本字符数
cost += (stats.ttsTextCharacters / 1000) * (costConfig['tts_per_1k_chars'] ?? 0.03);
// AI摘要成本(已废弃但保留兼容性)
cost += stats.aiSummaryApiCalls * (costConfig['ai_summary_per_request'] ?? 0.01);
return cost;
}
/// 计算翻译成本(单次调用)
double _calculateTranslationCost(int sourceCharacters) {
return (sourceCharacters / 1000) * (costConfig['translation_per_1k_chars'] ?? 0.05);
}
/// 计算ASR成本(单次调用)
double _calculateAsrCost(int durationSeconds) {
return (durationSeconds / 60) * (costConfig['asr_per_minute'] ?? 0.02);
}
/// 计算TTS成本(单次调用)
double _calculateTtsCost(int textCharacters) {
return (textCharacters / 1000) * (costConfig['tts_per_1k_chars'] ?? 0.03);
}
/// 保存当日统计数据
void _saveTodayStats() {
try {
final today = DateTime.now();
final todayKey = '${today.year}-${today.month.toString().padLeft(2, '0')}-${today.day.toString().padLeft(2, '0')}';
_storage.write('${_dailyStatsKey}_$todayKey', todayStats.value.toJson());
} catch (e) {
Logger.error('保存当日统计数据失败: $e');
}
}
/// 检查成本阈值
void _checkCostThreshold() {
final todayCost = todayStats.value.estimatedCost;
if (todayCost >= dailyCostThreshold.value) {
_showCostWarning('今日翻译成本已达到预警阈值:¥${todayCost.toStringAsFixed(2)}');
}
}
/// 显示成本预警
void _showCostWarning(String message) {
Get.snackbar(
'成本预警',
message,
snackPosition: SnackPosition.TOP,
backgroundColor: Get.theme.colorScheme.error.withOpacity(0.1),
colorText: Get.theme.colorScheme.error,
duration: const Duration(seconds: 5),
);
}
/// 获取历史统计数据
Future<List<TranslationUsageStats>> getHistoryStats({
required DateTime startDate,
required DateTime endDate,
}) async {
try {
final List<TranslationUsageStats> historyStats = [];
for (DateTime date = startDate;
date.isBefore(endDate) || date.isAtSameMomentAs(endDate);
date = date.add(const Duration(days: 1))) {
final dateKey = '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
final statsData = _storage.read('${_dailyStatsKey}_$dateKey');
if (statsData != null) {
historyStats.add(TranslationUsageStats.fromJson(Map<String, dynamic>.from(statsData)));
}
}
return historyStats;
} catch (e) {
Logger.error('获取历史统计数据失败: $e');
return [];
}
}
/// 清理过期数据(保留最近90天)
void cleanupOldData() {
try {
final cutoffDate = DateTime.now().subtract(const Duration(days: 90));
final allKeys = _storage.getKeys();
for (final key in allKeys) {
if (key.startsWith(_dailyStatsKey)) {
final dateStr = key.replaceFirst('${_dailyStatsKey}_', '');
try {
final date = DateTime.parse(dateStr);
if (date.isBefore(cutoffDate)) {
_storage.remove(key);
Logger.info('清理过期统计数据: $key');
}
} catch (e) {
// 忽略解析错误的键
}
}
}
} catch (e) {
Logger.error('清理过期数据失败: $e');
}
}
/// 更新成本配置
void updateCostConfig(Map<String, double> newConfig) {
costConfig.value = newConfig;
_saveCostConfig();
// 重新计算当日成本
todayStats.value = todayStats.value.copyWith(
estimatedCost: _calculateEstimatedCost(todayStats.value),
);
_saveTodayStats();
Logger.info('更新成本配置完成');
}
/// 更新预警阈值
void updateThresholds({
double? dailyThreshold,
double? monthlyThreshold,
}) {
if (dailyThreshold != null) {
dailyCostThreshold.value = dailyThreshold;
}
if (monthlyThreshold != null) {
monthlyCostThreshold.value = monthlyThreshold;
}
// 保存阈值配置
_storage.write('daily_cost_threshold', dailyCostThreshold.value);
_storage.write('monthly_cost_threshold', monthlyCostThreshold.value);
Logger.info('更新预警阈值完成');
}
/// 获取月度统计汇总
Future<TranslationUsageStats> getMonthlyStats(int year, int month) async {
try {
final startDate = DateTime(year, month, 1);
final endDate = DateTime(year, month + 1, 0); // 月末
final dailyStats = await getHistoryStats(
startDate: startDate,
endDate: endDate,
);
if (dailyStats.isEmpty) {
return TranslationUsageStats.empty(getCurrentUserId());
}
// 汇总月度数据
int totalTranslationCalls = 0;
int totalAsrCalls = 0;
int totalTtsCalls = 0;
int totalAiSummaryCalls = 0;
int totalSourceChars = 0;
int totalTargetChars = 0;
int totalRecordingDuration = 0;
int totalTtsPlayDuration = 0;
int totalSessions = 0;
double totalCost = 0.0;
final Map<String, int> combinedLanguagePairUsage = {};
final Map<String, int> combinedModeUsage = {};
for (final stats in dailyStats) {
totalTranslationCalls += stats.translationApiCalls;
totalAsrCalls += stats.asrApiCalls;
totalTtsCalls += stats.ttsApiCalls;
totalAiSummaryCalls += stats.aiSummaryApiCalls;
totalSourceChars += stats.sourceCharacterCount;
totalTargetChars += stats.targetCharacterCount;
totalRecordingDuration += stats.audioRecordingDuration;
totalTtsPlayDuration += stats.ttsPlaybackDuration;
totalSessions += stats.sessionCount;
totalCost += stats.estimatedCost;
// 合并语言对使用统计
stats.languagePairUsage.forEach((key, value) {
combinedLanguagePairUsage[key] = (combinedLanguagePairUsage[key] ?? 0) + value;
});
// 合并模式使用统计
stats.modeUsage.forEach((key, value) {
combinedModeUsage[key] = (combinedModeUsage[key] ?? 0) + value;
});
}
return TranslationUsageStats(
id: 'monthly_${year}_$month',
date: startDate,
userId: getCurrentUserId(),
timeGranularity: 'month',
translationApiCalls: totalTranslationCalls,
asrApiCalls: totalAsrCalls,
ttsApiCalls: totalTtsCalls,
aiSummaryApiCalls: totalAiSummaryCalls,
sourceTextCharacters: totalSourceChars,
effectiveAudioDuration: totalRecordingDuration,
ttsTextCharacters: totalTargetChars,
totalSessionDuration: totalTtsPlayDuration,
sessionCount: totalSessions,
estimatedCost: totalCost,
);
} catch (e) {
Logger.error('获取月度统计失败: $e');
return TranslationUsageStats.empty(getCurrentUserId());
}
}
}

42
lib/modules/agent/controllers/agent_controller.dart

@ -152,6 +152,48 @@ class AgentController extends GetxController {
_initMusicStatus(); //初始化音乐状态
_setupMediaSessionCallbacks(); //设置媒体会话回调
_setupTextInputListener(); //设置文本输入监听器
try {
// 1. 使用 GetX 获取 APP 当前语言环境 (Locale),而不是设备语言
final appLocale = Get.locale;
List<String> languagesToSupport = [];
if (appLocale != null) {
final lang = appLocale.languageCode; // e.g., 'en'
final country = appLocale.countryCode; // e.g., 'US'
// 2. 构造 Azure SDK 需要的格式 (e.g., 'en-US')
if (country != null && country.isNotEmpty) {
languagesToSupport.add('$lang-$country');
} else {
languagesToSupport.add(lang); // fallback only language code
}
}
// 3. 添加 en-US 作为备选
if (!languagesToSupport.contains('en-US')) {
languagesToSupport.add('en-US');
}
// 4. 最终兜底
if (languagesToSupport.isEmpty) {
languagesToSupport.add('en-US');
}
// 5. 调用原生设置
Logger.i(TAG, '支持的语言: ${languagesToSupport.join(', ')}');
AgentService.setSupportedLanguages(languagesToSupport).then((success) {
if (success) {
Logger.i(TAG, '支持的语言已根据 APP 设置语言: ${languagesToSupport.join(', ')}');
} else {
Logger.w(TAG, '根据 APP 设置语言失败');
}
});
} catch (e) {
Logger.e(TAG, "获取 APP 语言并设置失败: $e");
AgentService.setSupportedLanguages(['zh-CN', 'en-US']);
}
startVoiceInput(); //开启语音输入
}

2
lib/modules/agent/views/agent_view.dart

@ -530,7 +530,7 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
controller.isListening.value;
return Container(
height: 50.h,
height: 52.h,
padding: EdgeInsets.symmetric(
horizontal: 16.w),
decoration: BoxDecoration(

77
lib/modules/agent/views/message_bubble.dart

@ -33,8 +33,6 @@ class MessageBubble extends GetView<AgentController> {
@override
Widget build(BuildContext context) {
final timeFormat = DateFormat('HH:mm');
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
@ -91,7 +89,7 @@ class MessageBubble extends GetView<AgentController> {
children: [
// 时间戳(左对齐或右对齐)
Text(
timeFormat.format(timestamp),
_formatTimestamp(timestamp),
style: TextStyle(
fontSize: 10,
color: Theme.of(context).brightness == Brightness.dark
@ -105,7 +103,7 @@ class MessageBubble extends GetView<AgentController> {
Padding(
padding: const EdgeInsets.only(left: 8),
child: Text(
"generatedByAi".tr,// 由AI生成
"generatedByAi".tr, // 由AI生成
style: TextStyle(
fontSize: 8,
color: Colors.blue[600],
@ -418,6 +416,77 @@ class MessageBubble extends GetView<AgentController> {
Widget _buildDot(int milliseconds) {
return _AnimatedDot(delayMilliseconds: milliseconds);
}
/// 智能格式化时间戳
String _formatTimestamp(DateTime messageTime) {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final yesterday = today.subtract(const Duration(days: 1));
final messageDate =
DateTime(messageTime.year, messageTime.month, messageTime.day);
// 计算时间差
final difference = now.difference(messageTime);
// 1分钟内显示"刚刚"
if (difference.inMinutes < 1) {
return 'justNow'.tr; // 刚刚
}
// 1小时内显示"X分钟前"
if (difference.inHours < 1) {
return 'minutesAgo'
.trParams({'minutes': difference.inMinutes.toString()}); // X分钟前
}
// 今天的消息显示时间
if (messageDate == today) {
return DateFormat('HH:mm').format(messageTime);
}
// 昨天的消息显示"昨天 HH:mm"
if (messageDate == yesterday) {
return 'yesterday'.tr +
' ' +
DateFormat('HH:mm').format(messageTime); // 昨天 HH:mm
}
// 本周内的消息显示"星期X HH:mm"
if (difference.inDays < 7) {
final weekday = _getWeekdayName(messageTime.weekday);
return weekday + ' ' + DateFormat('HH:mm').format(messageTime);
}
// 今年的消息显示"MM-dd HH:mm"
if (messageTime.year == now.year) {
return DateFormat('MM-dd HH:mm').format(messageTime);
}
// 其他情况显示完整日期"yyyy-MM-dd HH:mm"
return DateFormat('yyyy-MM-dd HH:mm').format(messageTime);
}
/// 获取星期名称
String _getWeekdayName(int weekday) {
switch (weekday) {
case 1:
return 'monday'.tr; // 星期一
case 2:
return 'tuesday'.tr; // 星期二
case 3:
return 'wednesday'.tr; // 星期三
case 4:
return 'thursday'.tr; // 星期四
case 5:
return 'friday'.tr; // 星期五
case 6:
return 'saturday'.tr; // 星期六
case 7:
return 'sunday'.tr; // 星期日
default:
return '';
}
}
}
/// 动画点组件

5
lib/modules/explore/controllers/explore_controller.dart

@ -263,6 +263,11 @@ class ExploreController extends GetxController {
// 实际应用中,这里应该跳转到新功能页面
}
// 跳转到翻译统计页面
void goToTranslationStats() {
Get.toNamed(Routes.translationStats);
}
@override
void onClose() {
searchController.dispose();

72
lib/modules/explore/views/explore_view.dart

@ -115,12 +115,12 @@ class ExploreView extends GetView<ExploreController> {
),
child: IconButton(
icon: Icon(
Icons.more_vert,
Icons.analytics_outlined,
size: 16.sp,
color: isDarkMode ? Colors.white : Colors.grey[700],
),
padding: EdgeInsets.zero,
onPressed: () {},
onPressed: controller.goToTranslationStats,
),
),
],
@ -419,37 +419,38 @@ class ExploreView extends GetView<ExploreController> {
//整行布局
Widget _buildAIFeaturesWideSection(bool isDarkMode) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Text(
// 'aiFunction'.tr, // AI功能
// style: TextStyle(
// fontSize: 18.sp,
// fontWeight: FontWeight.w600,
// color: isDarkMode ? Colors.white : Colors.grey[800],
// ),
// ),
// SizedBox(height: 16.h),
// 直接使用 Column 包裹宽版卡片
Column(
children: controller.aiFeaturesWide.map((feature) {
return Padding(
padding: EdgeInsets.only(bottom: 16.h), // 卡片间距
child: _buildAIFeatureItemWide(
feature,
controller.aiFeatures.indexOf(feature),
isDarkMode,
),
);
}).toList(),
),
],
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Text(
// 'aiFunction'.tr, // AI功能
// style: TextStyle(
// fontSize: 18.sp,
// fontWeight: FontWeight.w600,
// color: isDarkMode ? Colors.white : Colors.grey[800],
// ),
// ),
// SizedBox(height: 16.h),
// 直接使用 Column 包裹宽版卡片
Column(
children: controller.aiFeaturesWide.map((feature) {
return Padding(
padding: EdgeInsets.only(bottom: 16.h), // 卡片间距
child: _buildAIFeatureItemWide(
feature,
controller.aiFeatures.indexOf(feature),
isDarkMode,
),
);
}).toList(),
),
],
);
}
// AI功能项(宽版本,用于横跨整行的AI全能助手)
Widget _buildAIFeatureItemWide(AIFeature feature, int index, bool isDarkMode) {
// AI功能项(宽版本,用于横跨整行的AI全能助手)
Widget _buildAIFeatureItemWide(
AIFeature feature, int index, bool isDarkMode) {
return GestureDetector(
onTap: () => controller.onAIFeatureTap(index),
child: Container(
@ -529,7 +530,8 @@ class ExploreView extends GetView<ExploreController> {
}
// AI功能项(宽版本,专门用于商务会议助手)
Widget _buildAIFeatureItemWideForMeeting(AIFeature feature, int index, bool isDarkMode) {
Widget _buildAIFeatureItemWideForMeeting(
AIFeature feature, int index, bool isDarkMode) {
return GestureDetector(
onTap: () => controller.onAIFeatureWideTap(index),
child: Container(
@ -646,7 +648,9 @@ class ExploreView extends GetView<ExploreController> {
child: Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withValues(alpha: 0.05) : Colors.grey[100],
color: isDarkMode
? Colors.white.withValues(alpha: 0.05)
: Colors.grey[100],
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(

257
lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart

@ -1,4 +1,6 @@
import 'package:classic_bluetooth/classic_bluetooth.dart';
import '/routes/app_routes.dart';
import 'package:logger/web.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:path/path.dart';
@ -121,7 +123,17 @@ class OtaUpgradeController extends GetxController {
// 实现更新逻辑...
} catch (e) {
errorMessage.value = e.toString();
Get.snackbar('更新失败', e.toString());
Get.snackbar(
'更新失败',
e.toString(),
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red[600],
colorText: Colors.white,
icon: const Icon(
Icons.error_outline,
color: Colors.white,
),
);
}
}
@ -166,7 +178,7 @@ class OtaUpgradeController extends GetxController {
// 从第三个 `.` 开始分割
final firstPart = firmwareParts.sublist(0, 3).join('.');
final secondPart = firmwareParts.sublist(3).join('.');
print("firstPart${firstPart},secondPart=${secondPart}");
Logger().d("firstPart${firstPart},secondPart=${secondPart}");
// 判断两部分是否一致
if (firstPart != secondPart) {
throw "固件版本格式错误:${deviceInfo.firmwareVersion}";
@ -174,7 +186,7 @@ class OtaUpgradeController extends GetxController {
// 正常流程
currentVersion.value = firstPart;
print('getequipmentinfo,id: ${deviceInfo.firmwareProductId}');
Logger().d('getequipmentinfo,id: ${deviceInfo.firmwareProductId}');
final connectedDevices = await ClassicBluetooth.getConnectedDevices();
if (connectedDevices.isNotEmpty) {
@ -192,7 +204,7 @@ class OtaUpgradeController extends GetxController {
}
final resp = OtaConfigResp.fromJson(response);
print('getequipmentinfo,resp.systemversion: ${resp.systemversion}');
Logger().d('getequipmentinfo,resp.systemversion: ${resp.systemversion}');
latestVersion.value = resp.systemversion!;
updatedescription.value = resp.updatedescription!;
updatepackageaddress = resp.updatepackageaddress!;
@ -210,10 +222,17 @@ class OtaUpgradeController extends GetxController {
} catch (e) {
// 统一错误处理
errorMessage.value = e.toString();
Get.snackbar('错误', e.toString(),
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white);
Get.snackbar(
'错误',
e.toString(),
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red[600],
colorText: Colors.white,
icon: const Icon(
Icons.error_outline,
color: Colors.white,
),
);
// 重要:重新抛出异常以便上层处理(可选)
} finally {
isFetchingDeviceInfo.value = false;
@ -237,7 +256,7 @@ class OtaUpgradeController extends GetxController {
}
return false;
} catch (e) {
print('版本比较错误: $e');
Logger().d('版本比较错误: $e');
return false;
}
}
@ -248,15 +267,35 @@ class OtaUpgradeController extends GetxController {
if (latestVersion.value != "") {
_showUpdateDialog(latestVersion.value);
} else {
Get.snackbar('错误', '获取最新版本信息失败');
Get.snackbar(
'错误',
'获取最新版本信息失败',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.orange[800],
colorText: Colors.white,
icon: const Icon(
Icons.error_outline,
color: Colors.white,
),
);
}
} catch (e) {
Get.snackbar('错误', '获取版本信息失败');
Get.snackbar(
'错误',
'获取版本信息失败',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.orange[800],
colorText: Colors.white,
icon: const Icon(
Icons.error_outline,
color: Colors.white,
),
);
}
}
void _showUpdateDialog(String latest) async {
print('显示更新对话框');
Logger().d('显示更新对话框');
Get.dialog(
AlertDialog(
title: Text('发现新版本 $latest'),
@ -264,7 +303,7 @@ class OtaUpgradeController extends GetxController {
actions: [
TextButton(
onPressed: () async {
print('取消按钮被点击');
Logger().d('取消按钮被点击');
Get.back();
Future.delayed(Duration(milliseconds: 300));
@ -273,7 +312,7 @@ class OtaUpgradeController extends GetxController {
),
TextButton(
onPressed: () async {
print('更新按钮被点击');
Logger().d('更新按钮被点击');
Get.back();
Future.delayed(Duration(milliseconds: 300));
@ -341,7 +380,17 @@ class OtaUpgradeController extends GetxController {
}
}
} catch (e) {
Get.snackbar('错误', '检查本地文件失败: $e');
Get.snackbar(
'错误',
'检查本地文件失败: $e',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red[600],
colorText: Colors.white,
icon: Icon(
Icons.error_outline,
color: Colors.white,
),
);
return;
}
@ -363,12 +412,22 @@ class OtaUpgradeController extends GetxController {
}
}
} catch (e) {
Get.snackbar('错误', '删除旧文件失败: $e');
Get.snackbar(
'错误',
'删除旧文件失败: $e',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red[600],
colorText: Colors.white,
icon: Icon(
Icons.error_outline,
color: Colors.white,
),
);
//isUpgrade.value = false;
return;
}
print('文件保存路径: $savePath');
Logger().d('文件保存路径: $savePath');
detailedStatus.value = '开始下载固件包...';
final options = Options(
@ -394,7 +453,17 @@ class OtaUpgradeController extends GetxController {
// Get.snackbar('下载完成', '文件已保存至: $savePath');
detailedStatus.value = '下载完成,准备升级...';
} else {
Get.snackbar('错误', '文件未找到,请检查写入权限');
Get.snackbar(
'错误',
'文件未找到,请检查写入权限',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.orange[600],
colorText: Colors.white,
icon: Icon(
Icons.error_outline,
color: Colors.white,
),
);
detailedStatus.value = '下载失败';
showDetailedStatus.value = false;
return;
@ -403,15 +472,45 @@ class OtaUpgradeController extends GetxController {
startUpgrading(savePath);
return;
} on DioException catch (e) {
Get.snackbar('下载失败', '错误: ${e.message}');
Get.snackbar(
'下载失败',
'错误: ${e.message}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.orange[600],
colorText: Colors.white,
icon: Icon(
Icons.error_outline,
color: Colors.white,
),
);
} on FileSystemException catch (e) {
Get.snackbar('存储失败', '无法写入文件: ${e.message}');
Get.snackbar(
'存储失败',
'无法写入文件: ${e.message}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.orange[600],
colorText: Colors.white,
icon: Icon(
Icons.error_outline,
color: Colors.white,
),
);
} catch (e) {
isDownloading.value = false;
detailedStatus.value = '下载失败,请检查网络连接';
showDetailedStatus.value = false;
Get.snackbar('下载失败', '请检查网络连接: $e');
print('下载失败: $e');
Get.snackbar(
'下载失败',
'请检查网络连接: $e',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.orange[600],
colorText: Colors.white,
icon: Icon(
Icons.error_outline,
color: Colors.white,
),
);
Logger().d('下载失败: $e');
}
}
@ -442,8 +541,12 @@ class OtaUpgradeController extends GetxController {
'电量检查失败',
'无法获取设备信息,请检查设备连接',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.orange,
backgroundColor: Colors.orange[600],
colorText: Colors.white,
icon: Icon(
Icons.warning_amber,
color: Colors.white,
),
);
return false;
}
@ -460,14 +563,36 @@ class OtaUpgradeController extends GetxController {
Get.dialog(
AlertDialog(
title: Text('电量不足'),
content: Text(message),
title: Text(
'电量不足',
style: TextStyle(
color: Colors.orange[700],
fontWeight: FontWeight.w600,
),
),
content: Text(
message,
style: TextStyle(
color: Colors.orange[600],
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text('确定'),
style: TextButton.styleFrom(
foregroundColor: Colors.orange[700],
),
child: const Text('确定'),
),
],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(
color: Colors.orange.withValues(alpha: 0.3),
width: 1,
),
),
backgroundColor: Colors.orange[50],
),
);
return false;
@ -479,8 +604,12 @@ class OtaUpgradeController extends GetxController {
'电量检查失败',
'检查设备电量时发生错误:$e',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
backgroundColor: Colors.orange[600],
colorText: Colors.white,
icon: const Icon(
Icons.warning_amber,
color: Colors.white,
),
);
return false;
}
@ -507,15 +636,83 @@ class OtaUpgradeController extends GetxController {
isUpgrade.value = false;
detailedStatus.value = '升级成功完成';
showDetailedStatus.value = false;
Get.snackbar('升级成功', '请退出重新连接设备');
_getCurrentVersion();
// 升级成功,更新版本信息显示
currentVersion.value = latestVersion.value;
isUpdateAvailable.value = false;
// 显示升级成功对话框,引导用户重新配对
Get.dialog(
AlertDialog(
title: Row(
children: [
Icon(
Icons.check_circle,
color: Colors.green[600],
size: 28,
),
SizedBox(width: 12),
Text(
'升级成功',
style: TextStyle(
color: Colors.green[700],
fontWeight: FontWeight.w600,
),
),
],
),
content: Text(
'固件升级已完成!\n\n设备需要重新配对才能正常使用,现在跳转到配对页面吗?',
style: TextStyle(
fontSize: 16,
height: 1.4,
),
),
actions: [
TextButton(
onPressed: () {
Get.back(); // 关闭对话框
},
child: Text(
'稍后配对',
style: TextStyle(color: Colors.grey[600]),
),
),
ElevatedButton(
onPressed: () {
Get.back(); // 关闭对话框
Get.toNamed(Routes.pairing);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green[600],
foregroundColor: Colors.white,
),
child: Text('立即配对'),
),
],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
barrierDismissible: false, // 防止用户意外关闭
);
}
if (event.event == 'onError') {
type.value = UPGRADE_FAILED;
isUpgrade.value = false;
detailedStatus.value = '升级失败,错误码:${event.code}';
showDetailedStatus.value = false;
Get.snackbar('升级失败', '请检查设备连接或升级包,错误码:${event.code}');
Get.snackbar(
'升级失败',
'请检查设备连接或升级包,错误码:${event.code}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red[600],
colorText: Colors.white,
icon: Icon(
Icons.error_outline,
color: Colors.white,
),
);
}
if (event.event == 'NOCONNECTION') {
type.value = CONNECTION_DISCONNECTION;

33
lib/modules/pairing/controllers/pairing_controller.dart

@ -384,29 +384,33 @@ class PairingController extends GetxController {
/// 检查当前状态并决定下一步
void _checkCurrentStatus() async {
try {
// 1. 检查蓝牙状态
if (_bluetoothManager.bluetoothState == 'off') {
// 使用 Future.wait 并行执行检查,提升启动速度
final results = await Future.wait([
_checkBluetoothState(), // 检查蓝牙状态
_checkPermissions(), // 检查权限
_bluetoothManager.refreshDeviceConnection(), // 刷新设备连接
]);
final isBluetoothOk = results[0] as bool;
final arePermissionsOk = results[1] as bool;
if (!isBluetoothOk) {
_setError(ErrorType.bluetoothOff, 'pleaseEnableBluetooth'.tr);
return;
}
// 2. 检查权限
if (!await _checkPermissions()) {
if (!arePermissionsOk) {
_setError(ErrorType.noPermission, 'pleaseGrantNecessaryPermissions'.tr);
return;
}
// 3. 先刷新设备连接状态,确保获取最新的连接信息
await _bluetoothManager.refreshDeviceConnection();
// 4. 检查经典蓝牙连接
// 后续逻辑保持不变,确保所有设备都经过验证
final classicDevice = _bluetoothManager.currentDevice;
if (classicDevice == null) {
_transition(PairingState.waitingClassic);
return;
}
// 5. 如果有设备,进行验证
_currentClassicDevice = classicDevice;
_transition(PairingState.verifying);
await _verifyDevice(classicDevice);
@ -416,6 +420,11 @@ class PairingController extends GetxController {
}
}
/// 新增一个辅助方法来检查蓝牙状态
Future<bool> _checkBluetoothState() async {
return _bluetoothManager.bluetoothState != 'off';
}
/// 检查权限
Future<bool> _checkPermissions() async {
// Android权限检查
@ -552,10 +561,10 @@ class PairingController extends GetxController {
try {
// 设置超时定时器
_bleTimeoutTimer?.cancel();
_bleTimeoutTimer = Timer(const Duration(seconds: 30), () {
_bleTimeoutTimer = Timer(const Duration(seconds: 20), () {
if (_currentState.value == PairingState.waitingBle) {
_setError(ErrorType.bleTimeout,
'bleConnectionTimeoutRetry'.tr); //BLE连接超时,请重试
'bleConnectionTimeoutRetry'.tr); //BLE连接超时,请重启耳机后再试
}
});
@ -700,7 +709,7 @@ class PairingController extends GetxController {
/// 导航到主页
void _navigateToHome() {
Future.delayed(const Duration(seconds: 1), () async {
Future.delayed(const Duration(milliseconds: 500), () async {
// 启动AgentService服务
try {
Logger.i(_tag, '启动AgentService服务');

342
lib/modules/pairing/views/pairing_view.dart

@ -3,9 +3,68 @@ import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../controllers/pairing_controller.dart';
class PairingView extends GetView<PairingController> {
class PairingView extends StatefulWidget {
const PairingView({Key? key}) : super(key: key);
@override
State<PairingView> createState() => _PairingViewState();
}
class _PairingViewState extends State<PairingView>
with TickerProviderStateMixin {
late AnimationController _pulseController;
late AnimationController _rotationController;
late Animation<double> _pulseAnimation;
late Animation<double> _rotationAnimation;
PairingController get controller => Get.find<PairingController>();
@override
void initState() {
super.initState();
// 脉搏动画控制器
_pulseController = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
);
// 旋转动画控制器
_rotationController = AnimationController(
duration: const Duration(milliseconds: 2000),
vsync: this,
);
// 脉搏动画
_pulseAnimation = Tween<double>(
begin: 0.8,
end: 1.2,
).animate(CurvedAnimation(
parent: _pulseController,
curve: Curves.easeInOut,
));
// 旋转动画
_rotationAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _rotationController,
curve: Curves.linear,
));
// 开始动画
_pulseController.repeat(reverse: true);
_rotationController.repeat();
}
@override
void dispose() {
_pulseController.dispose();
_rotationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final bool isDarkMode = Theme.of(context).brightness == Brightness.dark;
@ -30,7 +89,7 @@ class PairingView extends GetView<PairingController> {
padding: EdgeInsets.symmetric(horizontal: 24.w),
// 添加SingleChildScrollView以防止溢出
child: SingleChildScrollView(
physics: BouncingScrollPhysics(),
physics: const BouncingScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight:
@ -98,8 +157,8 @@ class PairingView extends GetView<PairingController> {
final state = controller.currentState;
final isLoading = controller.isLoading.value;
// 根据状态决定动画颜色
Color primaryColor = Color(0xFF0066CC);
Color secondaryColor = Color(0xFF5E5CE6);
Color primaryColor = const Color(0xFF0066CC);
Color secondaryColor = const Color(0xFF5E5CE6);
// 错误状态使用红色
if (state == PairingState.error) {
@ -114,7 +173,7 @@ class PairingView extends GetView<PairingController> {
// 待机状态的特殊处理
if (state == PairingState.idle || state == PairingState.waitingClassic) {
return Container(
return SizedBox(
width: 240.w,
height: 240.w,
child: Stack(
@ -129,8 +188,8 @@ class PairingView extends GetView<PairingController> {
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
primaryColor.withOpacity(0.1),
secondaryColor.withOpacity(0.1),
primaryColor.withValues(alpha: 0.1),
secondaryColor.withValues(alpha: 0.1),
],
),
shape: BoxShape.circle,
@ -159,51 +218,25 @@ class PairingView extends GetView<PairingController> {
);
}
return Container(
return SizedBox(
width: 240.w,
height: 240.w,
child: Stack(
alignment: Alignment.center,
children: [
// 脉搏背景 - 只在加载时显示动画
if (isLoading)
TweenAnimationBuilder<double>(
tween: Tween<double>(begin: 0.8, end: 1.1),
duration: const Duration(seconds: 2),
curve: Curves.easeInOut,
builder: (context, value, child) {
return Container(
width: 200.w * value,
height: 200.w * value,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
primaryColor.withOpacity(0.3),
secondaryColor.withOpacity(0.3),
],
),
shape: BoxShape.circle,
),
);
},
onEnd: () {
// 重新开始动画
},
),
// 连续脉搏动画 - 只在加载时显示
if (isLoading) _buildPulseAnimation(primaryColor, secondaryColor),
// 特殊的安全连接动画 - 只在建立安全连接时显示
if (state == PairingState.waitingBle)
_buildSecureConnectionAnimation(primaryColor, secondaryColor),
// 设备关联动画 - 只在关联设备时显示
if (state == PairingState.associating)
_buildAssociatingAnimation(primaryColor, secondaryColor),
// 旋转边框 - 只在加载时显示
if (isLoading)
SizedBox(
width: 100.w,
height: 100.w,
child: CircularProgressIndicator(
strokeWidth: 4.w,
valueColor: AlwaysStoppedAnimation<Color>(primaryColor),
backgroundColor: Colors.grey.withOpacity(0.2),
),
),
if (isLoading) _buildRotatingBorder(primaryColor),
// 设备图标
Container(
@ -229,6 +262,128 @@ class PairingView extends GetView<PairingController> {
});
}
// 构建连续脉搏动画
Widget _buildPulseAnimation(Color primaryColor, Color secondaryColor) {
return AnimatedBuilder(
animation: _pulseAnimation,
builder: (context, child) {
final value = _pulseAnimation.value;
final opacity = 0.4 - (_pulseController.value * 0.3); // 0.4 到 0.1 的透明度变化
return Container(
width: 200.w * value,
height: 200.w * value,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
primaryColor.withValues(alpha: opacity),
secondaryColor.withValues(alpha: opacity),
],
),
shape: BoxShape.circle,
),
);
},
);
}
// 构建旋转边框动画
Widget _buildRotatingBorder(Color primaryColor) {
return AnimatedBuilder(
animation: _rotationAnimation,
builder: (context, child) {
return Transform.rotate(
angle: _rotationAnimation.value * 2 * 3.14159, // 完整旋转
child: SizedBox(
width: 140.w,
height: 140.w,
child: CircularProgressIndicator(
strokeWidth: 3.w,
valueColor: AlwaysStoppedAnimation<Color>(primaryColor),
backgroundColor: Colors.grey.withValues(alpha: 0.2),
value: 0.7, // 显示70%的圆弧
),
),
);
},
);
}
// 构建安全连接特殊动画 - 多层波纹效果
Widget _buildSecureConnectionAnimation(Color primaryColor, Color secondaryColor) {
return Stack(
alignment: Alignment.center,
children: [
// 第一层波纹
AnimatedBuilder(
animation: _pulseAnimation,
builder: (context, child) {
final value = _pulseAnimation.value;
final opacity = 0.3 - (_pulseController.value * 0.2);
return Container(
width: 180.w * value,
height: 180.w * value,
decoration: BoxDecoration(
border: Border.all(
color: primaryColor.withValues(alpha: opacity),
width: 2.w,
),
shape: BoxShape.circle,
),
);
},
),
// 第二层波纹 - 延迟动画
AnimatedBuilder(
animation: _pulseAnimation,
builder: (context, child) {
final delayedValue = (_pulseController.value + 0.3) % 1.0;
final value = 0.8 + (delayedValue * 0.4);
final opacity = 0.25 - (delayedValue * 0.15);
return Container(
width: 160.w * value,
height: 160.w * value,
decoration: BoxDecoration(
border: Border.all(
color: secondaryColor.withValues(alpha: opacity),
width: 1.5.w,
),
shape: BoxShape.circle,
),
);
},
),
// 第三层波纹 - 更大延迟
AnimatedBuilder(
animation: _pulseAnimation,
builder: (context, child) {
final delayedValue = (_pulseController.value + 0.6) % 1.0;
final value = 0.8 + (delayedValue * 0.4);
final opacity = 0.2 - (delayedValue * 0.1);
return Container(
width: 140.w * value,
height: 140.w * value,
decoration: BoxDecoration(
border: Border.all(
color: primaryColor.withValues(alpha: opacity),
width: 1.w,
),
shape: BoxShape.circle,
),
);
},
),
],
);
}
// 根据状态获取图标
IconData _getIconForState(PairingState state) {
switch (state) {
@ -352,7 +507,7 @@ class PairingView extends GetView<PairingController> {
// 已连接设备信息
Widget _buildConnectedDevice(device, bool isDarkMode) {
return Container(
return SizedBox(
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -443,7 +598,7 @@ class PairingView extends GetView<PairingController> {
final showSkipButton = controller.showSkipButton.value;
final showAutoCodeInput = controller.showAutoCodeInput.value;
final showSkipBindingButton = controller.showSkipBindingButton.value;
return Container(
return SizedBox(
width: double.infinity,
child: Column(
children: [
@ -452,7 +607,7 @@ class PairingView extends GetView<PairingController> {
ElevatedButton(
onPressed: () => controller.onConnectTap(),
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF0066CC),
backgroundColor: const Color(0xFF0066CC),
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
@ -572,7 +727,7 @@ class PairingView extends GetView<PairingController> {
TextButton(
onPressed: () => controller.onSkipTap(),
style: TextButton.styleFrom(
foregroundColor: Color(0xFF0066CC),
foregroundColor: const Color(0xFF0066CC),
padding: EdgeInsets.symmetric(vertical: 12.h),
minimumSize: Size(double.infinity, 48.h),
),
@ -592,7 +747,7 @@ class PairingView extends GetView<PairingController> {
TextButton(
onPressed: () => controller.onSkipBindingTap(),
style: TextButton.styleFrom(
foregroundColor: Color(0xFF0066CC),
foregroundColor: const Color(0xFF0066CC),
padding: EdgeInsets.symmetric(vertical: 12.h),
minimumSize: Size(double.infinity, 48.h),
),
@ -611,7 +766,7 @@ class PairingView extends GetView<PairingController> {
TextButton(
onPressed: () => controller.onRetryTap(),
style: TextButton.styleFrom(
foregroundColor: Color(0xFF0066CC),
foregroundColor: const Color(0xFF0066CC),
padding: EdgeInsets.symmetric(vertical: 12.h),
minimumSize: Size(double.infinity, 48.h),
),
@ -629,4 +784,89 @@ class PairingView extends GetView<PairingController> {
);
});
}
// 构建设备关联动画 - 连接线条效果
Widget _buildAssociatingAnimation(Color primaryColor, Color secondaryColor) {
return AnimatedBuilder(
animation: _rotationAnimation,
builder: (context, child) {
return Stack(
alignment: Alignment.center,
children: [
// 旋转的连接线条
Transform.rotate(
angle: _rotationAnimation.value * 2 * 3.14159,
child: Container(
width: 160.w,
height: 160.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: primaryColor.withValues(alpha: 0.6),
width: 2.w,
),
),
child: Stack(
children: [
// 连接点1
Positioned(
top: 10.w,
left: 80.w - 4.w,
child: Container(
width: 8.w,
height: 8.w,
decoration: BoxDecoration(
color: primaryColor,
shape: BoxShape.circle,
),
),
),
// 连接点2
Positioned(
bottom: 10.w,
left: 80.w - 4.w,
child: Container(
width: 8.w,
height: 8.w,
decoration: BoxDecoration(
color: secondaryColor,
shape: BoxShape.circle,
),
),
),
// 连接点3
Positioned(
top: 80.w - 4.w,
left: 10.w,
child: Container(
width: 8.w,
height: 8.w,
decoration: BoxDecoration(
color: primaryColor,
shape: BoxShape.circle,
),
),
),
// 连接点4
Positioned(
top: 80.w - 4.w,
right: 10.w,
child: Container(
width: 8.w,
height: 8.w,
decoration: BoxDecoration(
color: secondaryColor,
shape: BoxShape.circle,
),
),
),
],
),
),
),
],
);
},
);
}
}

7
lib/modules/pairing/views/pairing_view_new.dart

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../controllers/pairing_controller.dart';
import '../../../core/themes/app_theme.dart';
/// 配对视图 - 简化版本
class PairingView extends GetView<PairingController> {
@ -94,9 +93,9 @@ class PairingView extends GetView<PairingController> {
Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
color: Colors.red.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12.r),
border: Border.all(color: Colors.red.withOpacity(0.3)),
border: Border.all(color: Colors.red.withValues(alpha: 0.3)),
),
child: Row(
children: [
@ -184,7 +183,7 @@ class PairingView extends GetView<PairingController> {
width: 100.w,
height: 100.w,
decoration: BoxDecoration(
color: iconColor.withOpacity(0.1),
color: iconColor.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: Icon(

9
lib/modules/profile/controllers/profile_controller.dart

@ -192,6 +192,15 @@ class ProfileController extends GetxController {
// }
// }
// 强制回滚未保存的更改(在页面退出时调用)
void forceRollbackIfUnsaved() {
if (hasEdits.value) {
Logger().d('检测到未保存的编辑,执行强制回滚');
_rollbackToOriginalState(); // 复用现有的回滚方法
}
isEditingNickname.value = false;
}
// 更新用户信息到后端
Future<void> _syncWithBackend() async {
try {

68
lib/modules/profile/views/profile_meeting_view.dart

@ -34,7 +34,10 @@ class ProfileMeetingView extends GetView<ProfileController> {
color: isDarkMode ? Colors.white : Colors.black87,
size: 20.w,
),
onPressed: () => Get.back(),
onPressed: () {
controller.forceRollbackIfUnsaved();
Get.back();
},
),
title: Text(
'账号和资料',
@ -92,35 +95,40 @@ class ProfileMeetingView extends GetView<ProfileController> {
}
Widget _buildBody(bool isDarkMode) {
return SafeArea(
child: ListView(
padding: EdgeInsets.symmetric(horizontal: 16.w),
children: [
SizedBox(height: 4.h),
_buildAvatarSection(isDarkMode),
SizedBox(height: 16.h),
_buildNicknameItem(isDarkMode),
Divider(
height: 0.5.h,
color: isDarkMode ? Colors.grey[800] : Colors.grey[200]),
_buildLoginMethodItem(isDarkMode),
Divider(
height: 0.5.h,
color: isDarkMode ? Colors.grey[800] : Colors.grey[200]),
_buildManageLoginItem(isDarkMode),
Divider(
height: 0.5.h,
color: isDarkMode ? Colors.grey[800] : Colors.grey[200]),
SizedBox(height: 32.h),
_buildLogoutButton(isDarkMode),
SizedBox(height: 16.h),
_buildDeleteAccountButton(isDarkMode),
SizedBox(height: 8.h),
_buildWarningText(),
SizedBox(height: 32.h),
],
),
);
return WillPopScope(
onWillPop: () async {
controller.forceRollbackIfUnsaved();
return true;
},
child: SafeArea(
child: ListView(
padding: EdgeInsets.symmetric(horizontal: 16.w),
children: [
SizedBox(height: 4.h),
_buildAvatarSection(isDarkMode),
SizedBox(height: 16.h),
_buildNicknameItem(isDarkMode),
Divider(
height: 0.5.h,
color: isDarkMode ? Colors.grey[800] : Colors.grey[200]),
_buildLoginMethodItem(isDarkMode),
Divider(
height: 0.5.h,
color: isDarkMode ? Colors.grey[800] : Colors.grey[200]),
_buildManageLoginItem(isDarkMode),
Divider(
height: 0.5.h,
color: isDarkMode ? Colors.grey[800] : Colors.grey[200]),
SizedBox(height: 32.h),
_buildLogoutButton(isDarkMode),
SizedBox(height: 16.h),
_buildDeleteAccountButton(isDarkMode),
SizedBox(height: 8.h),
_buildWarningText(),
SizedBox(height: 32.h),
],
),
));
}
Widget _buildAvatarSection(bool isDarkMode) {

31
lib/modules/profile/views/profile_view.dart

@ -158,7 +158,10 @@ class EditProfileView extends GetView<ProfileController> {
size: 20.sp,
color: isDarkMode ? Colors.white : Colors.black,
),
onPressed: () => Get.back(),
onPressed: () {
controller.forceRollbackIfUnsaved();
Get.back();
},
),
// title: Text(
// 'editProfile'.tr,
@ -214,15 +217,23 @@ class EditProfileView extends GetView<ProfileController> {
))
],
),
body: Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
child:
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
_buildEditProfileSection(context, isDarkMode, nameController),
SizedBox(height: 16.h),
_buildAccountSettings(isDarkMode),
SizedBox(height: 16.h),
])));
// ignore: deprecated_member_use
body: WillPopScope(
onWillPop: () async {
ProfileController.to.forceRollbackIfUnsaved();
return true;
},
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildEditProfileSection(
context, isDarkMode, nameController),
SizedBox(height: 16.h),
_buildAccountSettings(isDarkMode),
SizedBox(height: 16.h),
]))));
}
// 新增:构建编辑资料区域的方法

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

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

18
lib/modules/translation/bindings/translation_stats_binding.dart

@ -0,0 +1,18 @@
import 'package:get/get.dart';
import '../controllers/translation_stats_controller.dart';
import '../../../data/services/translation_usage_service.dart';
class TranslationStatsBinding implements Bindings {
@override
void dependencies() {
// 确保服务已注册
if (!Get.isRegistered<TranslationUsageService>()) {
Get.put(TranslationUsageService(), permanent: true);
}
// 注册控制器
Get.lazyPut<TranslationStatsController>(
() => TranslationStatsController(),
);
}
}

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

@ -1,4 +1,3 @@
import 'dart:ffi';
import 'dart:io';
import '../../../core/utils/permission_util.dart';
@ -15,8 +14,9 @@ import '../../../data/services/volcano_translation_service.dart';
import '../../../data/services/tts_service.dart';
import '../../../data/services/language_manager.dart';
import '../../../core/utils/logger.dart';
import '../../../data/models/events/voice_interaction_event.dart';
import '../../../data/services/asr_service.dart';
import '../../../data/services/translation_usage_service.dart';
import '../../../data/services/ble_manager.dart';
// 切换音频输出设备的枚举
@ -40,6 +40,15 @@ class TranslationController extends GetxController {
final bleManager = Get.find<BleManager>();
//音乐播放管理
final _musiceManager = Get.find<MusicManager>();
// 统计服务
late final TranslationUsageService _usageService;
// ASR统计相关 - 精确时长统计
DateTime? _asrSessionStartTime; // ASR会话开始时间
DateTime? _asrActiveStartTime; // ASR活跃开始时间(用户实际开始语音识别的时间)
Duration _asrActiveDuration = Duration.zero; // ASR累计活跃时长
bool _isAsrActive = false; // ASR是否处于活跃状态
// 音频输入源
bool _audioSourceType = false;
//是否开启播放
@ -122,11 +131,16 @@ class TranslationController extends GetxController {
// 监听识别结果
if (activeSpeaker.value != 0) {
isPlayback = false;
final recognitionStream =
await _asrService.startContinuousRecognition(_audioSourceType);
await _asrService.startContinuousRecognition(_audioSourceType);
// 开始ASR活跃时长跟踪(面对面模式)
_startAsrActiveTracking();
lastActiveSpeaker.value = id;
} else {
// 停止ASR活跃时长跟踪(面对面模式)
_stopAsrActiveTracking();
if (fTFTranslationResult != '' && !isPlayback) {
isPlayback = true;
await _ttsService.setAudioOutputDevice(lastActiveSpeaker.value);
@ -172,6 +186,9 @@ class TranslationController extends GetxController {
super.onInit();
//_setupEventListeners();
// 初始化统计服务
_initUsageService();
// 加载上次保存的语言设置
_loadSavedLanguages();
@ -198,6 +215,20 @@ class TranslationController extends GetxController {
// 参考Agent模块的实现模式
}
// 初始化统计服务
void _initUsageService() {
try {
// 确保统计服务已注册
if (!Get.isRegistered<TranslationUsageService>()) {
Get.put(TranslationUsageService(), permanent: true);
}
_usageService = Get.find<TranslationUsageService>();
Logger.info('翻译统计服务初始化完成');
} catch (e) {
Logger.error('翻译统计服务初始化失败: $e');
}
}
// 加载保存的语言设置
void _loadSavedLanguages() {
final savedSourceLanguage = _storage.read(_sourceLanguageKey);
@ -416,6 +447,17 @@ class TranslationController extends GetxController {
}
currentSessionId = DateTime.now().millisecondsSinceEpoch.toString();
// 开始新的翻译会话统计
try {
_usageService.startNewSession();
_asrSessionStartTime = DateTime.now(); // 记录ASR会话开始时间
_asrActiveDuration = Duration.zero; // 重置ASR活跃时长
_isAsrActive = false; // 重置ASR活跃状态
} catch (e) {
Logger.error('开始新会话统计失败: $e');
}
// 开始连续语音识别
if (currentMode.value == "audioVideo") {
bleManager.openDecoder();
@ -439,8 +481,9 @@ class TranslationController extends GetxController {
_asrService.stopContinuousRecognition();
} else {
await _ttsService.setAudioOutputDevice(0);
final recognitionStream =
await _asrService.startContinuousRecognition(_audioSourceType);
await _asrService.startContinuousRecognition(_audioSourceType);
// 开始ASR活跃时长计时
_startAsrActiveTracking();
}
isRecognizing.value = true;
if (isRecording.value && isRecognizing.value) {
@ -481,6 +524,9 @@ class TranslationController extends GetxController {
restoreOriginalAudioState();
await _asrService.stopContinuousRecognition();
// 停止ASR活跃时长计时
_stopAsrActiveTracking();
isRecognizing.value = false;
// 如果当前有中间结果,将其作为最终结果处理
@ -558,6 +604,8 @@ class TranslationController extends GetxController {
Future<void> handleFinalResult(String text) async {
if (text.isEmpty || translationHistory.isEmpty) return;
// ASR识别到最终结果,表示用户完成了一次有效的语音输入
// 更新当前项
if (translationHistory.last.isIntermediate) {
translationHistory.last.sourceText = text;
@ -660,6 +708,33 @@ class TranslationController extends GetxController {
// 【改进】只有在最终结果时才保存,避免中间态时的频繁保存
if (isFinal) {
saveTranslationHistory();
// 记录翻译API调用统计 - 优化版(精确计费参数)
try {
final sourceLanguageName = _languageManager
.getChineseNameByAsrCode(detectedSourceLanguageCode) ??
'未知';
final targetLanguageName = _languageManager
.getChineseNameByAsrCode(detectedTargetLanguageCode) ??
'未知';
// 使用精确的计费参数:只统计源文本字符数(翻译API的实际计费基础)
_usageService.recordTranslationApiCall(
sourceText: sourceText, // 只有源文本用于翻译API计费
targetText: translationResult, // 目标文本用于TTS计费
sourceLanguage: sourceLanguageName,
targetLanguage: targetLanguageName,
mode: currentMode.value,
);
Logger.info(
'翻译统计: 源文本${sourceText.length}字符, 模式${currentMode.value}, 语言对$sourceLanguageName→$targetLanguageName');
// 记录ASR统计(在翻译完成时统计ASR使用情况)
_recordAsrUsageStats(sourceLanguageName);
} catch (e) {
Logger.error('记录翻译统计失败: $e');
}
}
// 更新后滚动到底部
@ -705,9 +780,32 @@ class TranslationController extends GetxController {
final voiceName = _languageManager
.getTtsVoiceNameByAsrCode(detectedTargetLanguageCode) ??
'en-US-AriaNeural';
print("voiceName=$voiceName,text$text");
Logger.info("voiceName=$voiceName,text=$text");
final startTime = DateTime.now();
await _ttsService.setVoice(voiceName);
await _ttsService.speakOnce(text);
final endTime = DateTime.now();
// 记录TTS API调用统计 - 优化版(基于文本字符数计费)
try {
final durationSeconds = endTime.difference(startTime).inSeconds;
final languageName = _languageManager
.getChineseNameByAsrCode(detectedTargetLanguageCode) ??
'未知';
// TTS的主要计费依据是文本字符数,播放时长仅用于分析
_usageService.recordTtsApiCall(
text: text, // 文本字符数是TTS计费的主要依据
durationSeconds: durationSeconds, // 播放时长用于用户体验分析
language: languageName,
);
Logger.info(
'TTS统计: 文本${text.length}字符, 播放${durationSeconds}秒, 语言$languageName');
} catch (e) {
Logger.error('记录TTS统计失败: $e');
}
} catch (e) {
Logger.error('播放翻译失败: ${e.toString()}');
}
@ -724,6 +822,8 @@ class TranslationController extends GetxController {
if (isRecognizing.value) {
await stopRecognition();
}
// 确保停止ASR活跃时长跟踪
_stopAsrActiveTracking();
stopRecording();
_translationDebounceTimer?.cancel();
await _ttsService.stop();
@ -1210,6 +1310,54 @@ class TranslationController extends GetxController {
void scrollToBottom({bool animate = true}) {
_scrollToBottom(animate: animate);
}
/// 开始ASR活跃时长跟踪
void _startAsrActiveTracking() {
if (!_isAsrActive) {
_asrActiveStartTime = DateTime.now();
_isAsrActive = true;
Logger.info('开始ASR活跃时长跟踪');
}
}
/// 停止ASR活跃时长跟踪
void _stopAsrActiveTracking() {
if (_isAsrActive && _asrActiveStartTime != null) {
final activeDuration = DateTime.now().difference(_asrActiveStartTime!);
_asrActiveDuration += activeDuration;
_isAsrActive = false;
_asrActiveStartTime = null;
Logger.info('停止ASR活跃时长跟踪,本次活跃时长: ${activeDuration.inSeconds}秒');
}
}
/// 记录ASR使用统计 - 基于精确时长跟踪
void _recordAsrUsageStats(String language) {
try {
if (_asrSessionStartTime == null) return;
// 确保停止当前的活跃时长跟踪
_stopAsrActiveTracking();
final now = DateTime.now();
final totalDurationSeconds = now.difference(_asrSessionStartTime!).inSeconds;
final effectiveDurationSeconds = _asrActiveDuration.inSeconds;
_usageService.recordAsrApiCall(
effectiveDurationSeconds: effectiveDurationSeconds,
totalDurationSeconds: totalDurationSeconds,
language: language,
mode: currentMode.value,
audioFormat: 'pcm',
sampleRate: 16000,
);
Logger.info(
'ASR会话统计: 总时长${totalDurationSeconds}秒, 实际活跃时长${effectiveDurationSeconds}秒, 语言$language');
} catch (e) {
Logger.error('记录ASR统计失败: $e');
}
}
}
// 翻译项模型

258
lib/modules/translation/controllers/translation_cost_controller.dart

@ -0,0 +1,258 @@
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import '../../../data/services/translation_usage_service.dart';
import '../../../core/utils/logger.dart';
/// 翻译成本配置控制器
class TranslationCostController extends GetxController {
// 依赖注入
final TranslationUsageService _usageService = Get.find<TranslationUsageService>();
// 成本配置
final RxMap<String, double> costConfig = <String, double>{}.obs;
final RxDouble dailyCostThreshold = 10.0.obs;
final RxDouble monthlyCostThreshold = 300.0.obs;
// 表单控制器
final translationCostController = TextEditingController();
final asrCostController = TextEditingController();
final ttsCostController = TextEditingController();
final aiSummaryCostController = TextEditingController();
final dailyThresholdController = TextEditingController();
final monthlyThresholdController = TextEditingController();
// 状态变量
final isLoading = false.obs;
final isSaving = false.obs;
@override
void onInit() {
super.onInit();
_loadCurrentConfig();
}
@override
void onClose() {
// 释放控制器
translationCostController.dispose();
asrCostController.dispose();
ttsCostController.dispose();
aiSummaryCostController.dispose();
dailyThresholdController.dispose();
monthlyThresholdController.dispose();
super.onClose();
}
/// 加载当前配置
void _loadCurrentConfig() {
isLoading.value = true;
try {
// 从服务获取当前配置
costConfig.value = Map<String, double>.from(_usageService.costConfig);
dailyCostThreshold.value = _usageService.dailyCostThreshold.value;
monthlyCostThreshold.value = _usageService.monthlyCostThreshold.value;
// 更新表单控制器
translationCostController.text = (costConfig['translation_per_1k_chars'] ?? 0.05).toString();
asrCostController.text = (costConfig['asr_per_minute'] ?? 0.02).toString();
ttsCostController.text = (costConfig['tts_per_1k_chars'] ?? 0.03).toString();
aiSummaryCostController.text = (costConfig['ai_summary_per_request'] ?? 0.01).toString();
dailyThresholdController.text = dailyCostThreshold.value.toString();
monthlyThresholdController.text = monthlyCostThreshold.value.toString();
Logger.info('成本配置加载完成');
} catch (e) {
Logger.error('加载成本配置失败: $e');
_showErrorSnackbar('加载配置失败');
} finally {
isLoading.value = false;
}
}
/// 保存配置
Future<void> saveConfig() async {
if (isSaving.value) return;
isSaving.value = true;
try {
// 验证输入
if (!_validateInputs()) {
return;
}
// 更新成本配置
final newCostConfig = {
'translation_per_1k_chars': double.parse(translationCostController.text),
'asr_per_minute': double.parse(asrCostController.text),
'tts_per_1k_chars': double.parse(ttsCostController.text),
'ai_summary_per_request': double.parse(aiSummaryCostController.text),
};
final newDailyThreshold = double.parse(dailyThresholdController.text);
final newMonthlyThreshold = double.parse(monthlyThresholdController.text);
// 保存到服务
_usageService.updateCostConfig(newCostConfig);
_usageService.updateThresholds(
dailyThreshold: newDailyThreshold,
monthlyThreshold: newMonthlyThreshold,
);
// 更新本地状态
costConfig.value = newCostConfig;
dailyCostThreshold.value = newDailyThreshold;
monthlyCostThreshold.value = newMonthlyThreshold;
_showSuccessSnackbar('配置保存成功');
Logger.info('成本配置保存成功');
} catch (e) {
Logger.error('保存成本配置失败: $e');
_showErrorSnackbar('保存配置失败');
} finally {
isSaving.value = false;
}
}
/// 重置为默认配置
void resetToDefault() {
translationCostController.text = '0.05';
asrCostController.text = '0.02';
ttsCostController.text = '0.03';
aiSummaryCostController.text = '0.01';
dailyThresholdController.text = '10.0';
monthlyThresholdController.text = '300.0';
_showInfoSnackbar('已重置为默认配置,请点击保存生效');
}
/// 验证输入
bool _validateInputs() {
try {
final translationCost = double.parse(translationCostController.text);
final asrCost = double.parse(asrCostController.text);
final ttsCost = double.parse(ttsCostController.text);
final aiSummaryCost = double.parse(aiSummaryCostController.text);
final dailyThreshold = double.parse(dailyThresholdController.text);
final monthlyThreshold = double.parse(monthlyThresholdController.text);
if (translationCost < 0 || asrCost < 0 || ttsCost < 0 || aiSummaryCost < 0) {
_showErrorSnackbar('成本不能为负数');
return false;
}
if (dailyThreshold < 0 || monthlyThreshold < 0) {
_showErrorSnackbar('预警阈值不能为负数');
return false;
}
if (dailyThreshold > monthlyThreshold) {
_showErrorSnackbar('日预警阈值不能大于月预警阈值');
return false;
}
return true;
} catch (e) {
_showErrorSnackbar('请输入有效的数字');
return false;
}
}
/// 计算预估月成本
double calculateEstimatedMonthlyCost() {
try {
final dailyStats = _usageService.todayStats.value;
if (dailyStats.estimatedCost == 0) return 0.0;
// 简单按30天计算
return dailyStats.estimatedCost * 30;
} catch (e) {
Logger.error('计算预估月成本失败: $e');
return 0.0;
}
}
/// 获取成本节约建议
List<String> getCostSavingTips() {
final tips = <String>[];
try {
final dailyStats = _usageService.todayStats.value;
// 基于使用情况给出建议
if (dailyStats.translationApiCalls > 100) {
tips.add('考虑批量翻译以减少API调用次数');
}
if (dailyStats.ttsApiCalls > 50) {
tips.add('可以关闭自动语音播放以节省TTS成本');
}
if (dailyStats.aiSummaryApiCalls > 10) {
tips.add('AI摘要功能按需使用,避免频繁调用');
}
if (dailyStats.effectiveAudioDuration > 3600) { // 超过1小时
tips.add('长时间录音会增加ASR成本,建议分段录音');
}
if (tips.isEmpty) {
tips.add('您的使用习惯很好,继续保持!');
}
} catch (e) {
Logger.error('获取成本节约建议失败: $e');
tips.add('暂无建议');
}
return tips;
}
/// 显示成功提示
void _showSuccessSnackbar(String message) {
Get.snackbar(
'成功',
message,
snackPosition: SnackPosition.TOP,
backgroundColor: Get.theme.colorScheme.primary.withOpacity(0.1),
colorText: Get.theme.colorScheme.primary,
duration: const Duration(seconds: 3),
);
}
/// 显示错误提示
void _showErrorSnackbar(String message) {
Get.snackbar(
'错误',
message,
snackPosition: SnackPosition.TOP,
backgroundColor: Get.theme.colorScheme.error.withOpacity(0.1),
colorText: Get.theme.colorScheme.error,
duration: const Duration(seconds: 3),
);
}
/// 显示信息提示
void _showInfoSnackbar(String message) {
Get.snackbar(
'提示',
message,
snackPosition: SnackPosition.TOP,
backgroundColor: Get.theme.colorScheme.surface.withOpacity(0.9),
colorText: Get.theme.colorScheme.onSurface,
duration: const Duration(seconds: 3),
);
}
/// 格式化成本显示
String formatCost(double cost) {
return '¥${cost.toStringAsFixed(2)}';
}
/// 格式化百分比显示
String formatPercentage(double percentage) {
return '${percentage.toStringAsFixed(1)}%';
}
}

19
lib/modules/translation/controllers/translation_detail_controller.dart

@ -6,11 +6,13 @@ import '../controllers/translation_history_controller.dart';
import 'package:get_storage/get_storage.dart';
import 'dart:async';
import '../../../core/utils/logger.dart';
import '../../../data/services/translation_usage_service.dart';
class TranslationDetailController extends GetxController {
// 依赖注入
final VolcanoAIService _aiService = Get.find<VolcanoAIService>();
final GetStorage _storage = GetStorage();
late final TranslationUsageService _usageService;
// 存储键前缀
static const String _summaryKeyPrefix = 'translation_summary_';
@ -34,6 +36,16 @@ class TranslationDetailController extends GetxController {
void onInit() {
super.onInit();
// 初始化统计服务
try {
if (!Get.isRegistered<TranslationUsageService>()) {
Get.put(TranslationUsageService(), permanent: true);
}
_usageService = Get.find<TranslationUsageService>();
} catch (e) {
Logger.error('初始化统计服务失败: $e');
}
// 从路由参数中获取数据
if (Get.arguments != null) {
currentItem = Get.arguments['item'];
@ -170,6 +182,13 @@ class TranslationDetailController extends GetxController {
if (result.isNotEmpty) {
summaryResult.value = result;
saveSummary(result);
// 记录AI摘要API调用统计
try {
_usageService.recordAiSummaryApiCall();
} catch (e) {
Logger.error('记录AI摘要统计失败: $e');
}
}
return result;

358
lib/modules/translation/controllers/translation_stats_controller.dart

@ -0,0 +1,358 @@
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import '../../../data/services/translation_usage_service.dart';
import '../../../data/models/translation_usage_stats.dart';
import '../../../core/utils/logger.dart';
/// 翻译统计页面控制器
class TranslationStatsController extends GetxController {
// 依赖注入
final TranslationUsageService _usageService =
Get.find<TranslationUsageService>();
// 状态变量
final isLoading = false.obs;
final selectedTabIndex = 0.obs; // 0=今日, 1=本周, 2=本月, 3=历史
final selectedTimeRange = '今日'.obs;
// 统计数据
final Rx<TranslationUsageStats> todayStats =
TranslationUsageStats.empty('').obs;
final Rx<TranslationUsageStats> weekStats =
TranslationUsageStats.empty('').obs;
final Rx<TranslationUsageStats> monthStats =
TranslationUsageStats.empty('').obs;
final RxList<TranslationUsageStats> historyStats =
<TranslationUsageStats>[].obs;
// 实时统计数据
RealtimeUsageStats get realtimeStats => _usageService.realtimeStats.value;
// 成本配置
final RxMap<String, double> costConfig = <String, double>{}.obs;
final RxDouble dailyCostThreshold = 10.0.obs;
final RxDouble monthlyCostThreshold = 300.0.obs;
// 图表数据
final RxList<Map<String, dynamic>> chartData = <Map<String, dynamic>>[].obs;
@override
void onInit() {
super.onInit();
_initializeData();
}
/// 初始化数据
void _initializeData() {
// 监听使用服务的数据变化
ever(_usageService.todayStats, (stats) {
todayStats.value = stats;
_updateChartData();
});
ever(_usageService.costConfig, (config) {
costConfig.value = config;
});
ever(_usageService.dailyCostThreshold, (threshold) {
dailyCostThreshold.value = threshold;
});
ever(_usageService.monthlyCostThreshold, (threshold) {
monthlyCostThreshold.value = threshold;
});
// 初始加载数据
loadAllStats();
}
/// 加载所有统计数据
Future<void> loadAllStats() async {
isLoading.value = true;
try {
// 加载今日数据
todayStats.value = _usageService.todayStats.value;
// 加载本周数据
await _loadWeekStats();
// 加载本月数据
await _loadMonthStats();
// 加载历史数据(最近7天)
await _loadHistoryStats();
// 更新图表数据
_updateChartData();
Logger.info('统计数据加载完成');
} catch (e) {
Logger.error('加载统计数据失败: $e');
Get.snackbar(
'加载失败',
'统计数据加载失败,请稍后重试',
snackPosition: SnackPosition.TOP,
backgroundColor: Get.theme.colorScheme.error.withOpacity(0.1),
colorText: Get.theme.colorScheme.error,
);
} finally {
isLoading.value = false;
}
}
/// 加载本周统计数据
Future<void> _loadWeekStats() async {
try {
final now = DateTime.now();
final weekStart = now.subtract(Duration(days: now.weekday - 1));
final weekEnd = weekStart.add(const Duration(days: 6));
final dailyStats = await _usageService.getHistoryStats(
startDate: weekStart,
endDate: weekEnd,
);
weekStats.value = _aggregateStats(dailyStats, 'week');
} catch (e) {
Logger.error('加载本周统计失败: $e');
}
}
/// 加载本月统计数据
Future<void> _loadMonthStats() async {
try {
final now = DateTime.now();
monthStats.value =
await _usageService.getMonthlyStats(now.year, now.month);
} catch (e) {
Logger.error('加载本月统计失败: $e');
}
}
/// 加载历史统计数据
Future<void> _loadHistoryStats() async {
try {
final endDate = DateTime.now();
final startDate = endDate.subtract(const Duration(days: 7));
historyStats.value = await _usageService.getHistoryStats(
startDate: startDate,
endDate: endDate,
);
// 按日期排序
historyStats.sort((a, b) => b.date.compareTo(a.date));
} catch (e) {
Logger.error('加载历史统计失败: $e');
}
}
/// 聚合统计数据
TranslationUsageStats _aggregateStats(
List<TranslationUsageStats> statsList, String period) {
if (statsList.isEmpty) {
return TranslationUsageStats.empty(_usageService.getCurrentUserId());
}
int totalTranslationCalls = 0;
int totalAsrCalls = 0;
int totalTtsCalls = 0;
int totalAiSummaryCalls = 0;
int totalSourceChars = 0;
int totalTargetChars = 0;
int totalRecordingDuration = 0;
int totalTtsPlayDuration = 0;
int totalSessions = 0;
double totalCost = 0.0;
final Map<String, int> combinedLanguagePairUsage = {};
final Map<String, int> combinedModeUsage = {};
for (final stats in statsList) {
totalTranslationCalls += stats.translationApiCalls;
totalAsrCalls += stats.asrApiCalls;
totalTtsCalls += stats.ttsApiCalls;
totalAiSummaryCalls += stats.aiSummaryApiCalls;
totalSourceChars += stats.sourceTextCharacters;
totalTargetChars += stats.ttsTextCharacters;
totalRecordingDuration += stats.effectiveAudioDuration;
totalTtsPlayDuration += stats.totalSessionDuration;
totalSessions += stats.sessionCount;
totalCost += stats.estimatedCost;
// 合并语言对使用统计(使用兼容性getter)
stats.languagePairUsage.forEach((key, value) {
combinedLanguagePairUsage[key] =
(combinedLanguagePairUsage[key] ?? 0) + value;
});
// 合并模式使用统计(使用兼容性getter)
stats.modeUsage.forEach((key, value) {
combinedModeUsage[key] = (combinedModeUsage[key] ?? 0) + value;
});
}
return TranslationUsageStats(
id: '${period}_${DateTime.now().millisecondsSinceEpoch}',
date: DateTime.now(),
userId: 'current_user',
timeGranularity: period,
translationApiCalls: totalTranslationCalls,
asrApiCalls: totalAsrCalls,
ttsApiCalls: totalTtsCalls,
aiSummaryApiCalls: totalAiSummaryCalls,
sourceTextCharacters: totalSourceChars,
ttsTextCharacters: totalTargetChars,
effectiveAudioDuration: totalRecordingDuration,
totalSessionDuration: totalTtsPlayDuration,
sessionCount: totalSessions,
estimatedCost: totalCost,
);
}
/// 更新图表数据
void _updateChartData() {
final List<Map<String, dynamic>> data = [];
// 根据选中的时间范围生成图表数据
switch (selectedTabIndex.value) {
case 0: // 今日
data.add({
'label': '今日',
'apiCalls': todayStats.value.totalApiCalls,
'cost': todayStats.value.estimatedCost,
'characters': todayStats.value.totalCharacterCount,
});
break;
case 1: // 本周
data.add({
'label': '本周',
'apiCalls': weekStats.value.totalApiCalls,
'cost': weekStats.value.estimatedCost,
'characters': weekStats.value.totalCharacterCount,
});
break;
case 2: // 本月
data.add({
'label': '本月',
'apiCalls': monthStats.value.totalApiCalls,
'cost': monthStats.value.estimatedCost,
'characters': monthStats.value.totalCharacterCount,
});
break;
case 3: // 历史
for (final stats in historyStats.take(7)) {
data.add({
'label': '${stats.date.month}/${stats.date.day}',
'apiCalls': stats.totalApiCalls,
'cost': stats.estimatedCost,
'characters': stats.totalCharacterCount,
});
}
break;
}
chartData.value = data;
}
/// 切换时间范围
void switchTimeRange(int index) {
selectedTabIndex.value = index;
switch (index) {
case 0:
selectedTimeRange.value = '今日';
break;
case 1:
selectedTimeRange.value = '本周';
break;
case 2:
selectedTimeRange.value = '本月';
break;
case 3:
selectedTimeRange.value = '历史';
break;
}
_updateChartData();
}
/// 获取当前显示的统计数据
TranslationUsageStats get currentStats {
switch (selectedTabIndex.value) {
case 0:
return todayStats.value;
case 1:
return weekStats.value;
case 2:
return monthStats.value;
default:
return todayStats.value;
}
}
/// 刷新数据
Future<void> refreshData() async {
await loadAllStats();
}
/// 清理过期数据
void cleanupOldData() {
_usageService.cleanupOldData();
Get.snackbar(
'清理完成',
'已清理90天前的过期数据',
snackPosition: SnackPosition.TOP,
backgroundColor: Get.theme.colorScheme.primary.withOpacity(0.1),
colorText: Get.theme.colorScheme.primary,
);
}
/// 导出统计数据
Future<void> exportStats() async {
try {
// TODO: 实现数据导出功能
Get.snackbar(
'导出功能',
'数据导出功能正在开发中',
snackPosition: SnackPosition.TOP,
backgroundColor: Get.theme.colorScheme.primary.withOpacity(0.1),
colorText: Get.theme.colorScheme.primary,
);
} catch (e) {
Logger.error('导出统计数据失败: $e');
}
}
/// 格式化时长显示
String formatDuration(int seconds) {
if (seconds < 60) {
return '${seconds}秒';
} else if (seconds < 3600) {
final minutes = seconds ~/ 60;
final remainingSeconds = seconds % 60;
return '${minutes}分${remainingSeconds}秒';
} else {
final hours = seconds ~/ 3600;
final minutes = (seconds % 3600) ~/ 60;
return '${hours}小时${minutes}分';
}
}
/// 格式化成本显示
String formatCost(double cost) {
return '¥${cost.toStringAsFixed(2)}';
}
/// 格式化数字显示
String formatNumber(int number) {
if (number < 1000) {
return number.toString();
} else if (number < 1000000) {
return '${(number / 1000).toStringAsFixed(1)}K';
} else {
return '${(number / 1000000).toStringAsFixed(1)}M';
}
}
}

2
lib/modules/translation/views/translation_detail_view.dart

@ -110,7 +110,7 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
isDarkMode: isDarkMode,
)),
),
//屏蔽“记录摘要”,需要是开启
//屏蔽“记录摘要”,需要时开启
// SizedBox(width: 12.w),
// Expanded(
// child: Obx(() => _buildTabButton(

757
lib/modules/translation/views/translation_stats_view.dart

@ -0,0 +1,757 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../controllers/translation_stats_controller.dart';
import '../../../core/theme/app_colors.dart';
/// 翻译统计页面
class TranslationStatsView extends GetView<TranslationStatsController> {
const TranslationStatsView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.grey[50],
appBar: _buildAppBar(isDarkMode),
body: Obx(() => controller.isLoading.value
? _buildLoadingWidget()
: _buildBody(isDarkMode)),
);
}
/// 构建应用栏
PreferredSizeWidget _buildAppBar(bool isDarkMode) {
return AppBar(
backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.white,
elevation: 0.5,
title: Text(
'翻译统计', // translationStats
style: TextStyle(
fontSize: 17.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
centerTitle: true,
actions: [
// 刷新按钮
IconButton(
icon: Icon(
Icons.refresh,
color: isDarkMode ? Colors.white : Colors.black,
),
onPressed: controller.refreshData,
),
// 更多操作菜单
PopupMenuButton<String>(
icon: Icon(
Icons.more_vert,
color: isDarkMode ? Colors.white : Colors.black,
),
onSelected: (value) {
switch (value) {
case 'cleanup':
controller.cleanupOldData();
break;
case 'export':
controller.exportStats();
break;
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'cleanup',
child: Text('清理过期数据'),
),
const PopupMenuItem(
value: 'export',
child: Text('导出数据'),
),
],
),
],
);
}
/// 构建加载组件
Widget _buildLoadingWidget() {
return const Center(
child: CircularProgressIndicator(),
);
}
/// 构建主体内容
Widget _buildBody(bool isDarkMode) {
return Column(
children: [
// 时间范围选择器
_buildTimeRangeSelector(isDarkMode),
// 统计内容
Expanded(
child: SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: Column(
children: [
// 实时统计卡片
if (controller.selectedTabIndex.value == 0)
_buildRealtimeStatsCard(isDarkMode),
SizedBox(height: 16.h),
// 概览统计卡片
_buildOverviewCard(isDarkMode),
SizedBox(height: 16.h),
// API调用统计
_buildApiCallsCard(isDarkMode),
SizedBox(height: 16.h),
// 数据量统计
_buildDataVolumeCard(isDarkMode),
SizedBox(height: 16.h),
// 成本统计
_buildCostCard(isDarkMode),
SizedBox(height: 16.h),
// 使用行为统计
_buildUsageBehaviorCard(isDarkMode),
],
),
),
),
],
);
}
/// 构建时间范围选择器
Widget _buildTimeRangeSelector(bool isDarkMode) {
return Container(
margin: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: isDarkMode ? AppColors.cardBackground : Colors.white,
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Obx(() => Row(
children: [
_buildTimeRangeTab('今日', 0, isDarkMode),
_buildTimeRangeTab('本周', 1, isDarkMode),
_buildTimeRangeTab('本月', 2, isDarkMode),
_buildTimeRangeTab('历史', 3, isDarkMode),
],
)),
);
}
/// 构建时间范围标签
Widget _buildTimeRangeTab(String title, int index, bool isDarkMode) {
final isSelected = controller.selectedTabIndex.value == index;
return Expanded(
child: GestureDetector(
onTap: () => controller.switchTimeRange(index),
child: Container(
padding: EdgeInsets.symmetric(vertical: 12.h),
decoration: BoxDecoration(
color: isSelected
? (isDarkMode ? AppColors.primary : AppColors.primary)
: Colors.transparent,
borderRadius: BorderRadius.circular(12.r),
),
child: Text(
title,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.sp,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
color: isSelected
? Colors.white
: (isDarkMode ? Colors.white70 : Colors.black54),
),
),
),
),
);
}
/// 构建实时统计卡片
Widget _buildRealtimeStatsCard(bool isDarkMode) {
return Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: isDarkMode ? AppColors.cardBackground : Colors.white,
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.access_time,
color: AppColors.primary,
size: 20.sp,
),
SizedBox(width: 8.w),
Text(
'当前会话统计',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
],
),
SizedBox(height: 16.h),
Obx(() => Row(
children: [
Expanded(
child: _buildStatItem(
'翻译次数',
controller.realtimeStats.translationCount.toString(),
Icons.translate,
isDarkMode,
),
),
Expanded(
child: _buildStatItem(
'会话时长',
'${controller.realtimeStats.sessionDurationMinutes}分钟',
Icons.timer,
isDarkMode,
),
),
],
)),
SizedBox(height: 12.h),
Obx(() => Row(
children: [
Expanded(
child: _buildStatItem(
'字符数',
controller.formatNumber(
controller.realtimeStats.sourceCharacters +
controller.realtimeStats.targetCharacters,
),
Icons.text_fields,
isDarkMode,
),
),
Expanded(
child: _buildStatItem(
'录音时长',
controller.formatDuration(controller.realtimeStats.recordingDuration),
Icons.mic,
isDarkMode,
),
),
],
)),
],
),
);
}
/// 构建概览卡片
Widget _buildOverviewCard(bool isDarkMode) {
return Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: isDarkMode ? AppColors.cardBackground : Colors.white,
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.dashboard,
color: AppColors.primary,
size: 20.sp,
),
SizedBox(width: 8.w),
Obx(() => Text(
'${controller.selectedTimeRange.value}概览',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
)),
],
),
SizedBox(height: 16.h),
Obx(() => Row(
children: [
Expanded(
child: _buildStatItem(
'总调用次数',
controller.formatNumber(controller.currentStats.totalApiCalls),
Icons.api,
isDarkMode,
),
),
Expanded(
child: _buildStatItem(
'预估成本',
controller.formatCost(controller.currentStats.estimatedCost),
Icons.attach_money,
isDarkMode,
),
),
],
)),
SizedBox(height: 12.h),
Obx(() => Row(
children: [
Expanded(
child: _buildStatItem(
'会话数量',
controller.currentStats.sessionCount.toString(),
Icons.chat,
isDarkMode,
),
),
Expanded(
child: _buildStatItem(
'总字符数',
controller.formatNumber(controller.currentStats.totalCharacterCount),
Icons.text_snippet,
isDarkMode,
),
),
],
)),
],
),
);
}
/// 构建统计项
Widget _buildStatItem(String label, String value, IconData icon, bool isDarkMode) {
return Column(
children: [
Icon(
icon,
color: AppColors.primary,
size: 24.sp,
),
SizedBox(height: 8.h),
Text(
value,
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black,
),
),
SizedBox(height: 4.h),
Text(
label,
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
],
);
}
/// 构建API调用统计卡片
Widget _buildApiCallsCard(bool isDarkMode) {
return Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: isDarkMode ? AppColors.cardBackground : Colors.white,
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.api,
color: AppColors.primary,
size: 20.sp,
),
SizedBox(width: 8.w),
Text(
'API调用统计',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
],
),
SizedBox(height: 16.h),
Obx(() => Column(
children: [
_buildApiCallRow('翻译API', controller.currentStats.translationApiCalls, Icons.translate, isDarkMode),
SizedBox(height: 12.h),
_buildApiCallRow('语音识别', controller.currentStats.asrApiCalls, Icons.mic, isDarkMode),
SizedBox(height: 12.h),
_buildApiCallRow('语音合成', controller.currentStats.ttsApiCalls, Icons.volume_up, isDarkMode),
SizedBox(height: 12.h),
_buildApiCallRow('AI摘要', controller.currentStats.aiSummaryApiCalls, Icons.summarize, isDarkMode),
],
)),
],
),
);
}
/// 构建API调用行
Widget _buildApiCallRow(String label, int count, IconData icon, bool isDarkMode) {
return Row(
children: [
Icon(
icon,
color: AppColors.primary,
size: 18.sp,
),
SizedBox(width: 12.w),
Expanded(
child: Text(
label,
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black,
),
),
),
Text(
controller.formatNumber(count),
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
],
);
}
/// 构建数据量统计卡片
Widget _buildDataVolumeCard(bool isDarkMode) {
return Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: isDarkMode ? AppColors.cardBackground : Colors.white,
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.data_usage,
color: AppColors.primary,
size: 20.sp,
),
SizedBox(width: 8.w),
Text(
'数据量统计',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
],
),
SizedBox(height: 16.h),
Obx(() => Column(
children: [
Row(
children: [
Expanded(
child: _buildStatItem(
'源文本字符',
controller.formatNumber(controller.currentStats.sourceTextCharacters),
Icons.text_fields,
isDarkMode,
),
),
Expanded(
child: _buildStatItem(
'TTS文本字符',
controller.formatNumber(controller.currentStats.ttsTextCharacters),
Icons.translate,
isDarkMode,
),
),
],
),
SizedBox(height: 16.h),
Row(
children: [
Expanded(
child: _buildStatItem(
'有效音频时长',
controller.formatDuration(controller.currentStats.effectiveAudioDuration),
Icons.mic,
isDarkMode,
),
),
Expanded(
child: _buildStatItem(
'总会话时长',
controller.formatDuration(controller.currentStats.totalSessionDuration),
Icons.volume_up,
isDarkMode,
),
),
],
),
],
)),
],
),
);
}
/// 构建成本统计卡片
Widget _buildCostCard(bool isDarkMode) {
return Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: isDarkMode ? AppColors.cardBackground : Colors.white,
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.attach_money,
color: AppColors.primary,
size: 20.sp,
),
SizedBox(width: 8.w),
Text(
'成本统计',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
],
),
SizedBox(height: 16.h),
Obx(() => Column(
children: [
// 当前成本
Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: AppColors.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(8.r),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${controller.selectedTimeRange.value}预估成本',
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.white : Colors.black,
),
),
Text(
controller.formatCost(controller.currentStats.estimatedCost),
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
color: AppColors.primary,
),
),
],
),
),
SizedBox(height: 12.h),
// 预警阈值
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'日成本预警阈值',
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
Text(
controller.formatCost(controller.dailyCostThreshold.value),
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
],
),
SizedBox(height: 4.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'月成本预警阈值',
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
Text(
controller.formatCost(controller.monthlyCostThreshold.value),
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
],
),
],
)),
],
),
);
}
/// 构建使用行为统计卡片
Widget _buildUsageBehaviorCard(bool isDarkMode) {
return Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: isDarkMode ? AppColors.cardBackground : Colors.white,
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.analytics,
color: AppColors.primary,
size: 20.sp,
),
SizedBox(width: 8.w),
Text(
'使用行为统计',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
],
),
SizedBox(height: 16.h),
Obx(() => Column(
children: [
// 最常用语言对
_buildUsageBehaviorRow(
'最常用语言对',
controller.currentStats.mostUsedLanguagePair,
Icons.language,
isDarkMode,
),
SizedBox(height: 12.h),
// 最常用翻译模式
_buildUsageBehaviorRow(
'最常用模式',
controller.currentStats.mostUsedMode,
Icons.settings,
isDarkMode,
),
],
)),
],
),
);
}
/// 构建使用行为行
Widget _buildUsageBehaviorRow(String label, String value, IconData icon, bool isDarkMode) {
return Row(
children: [
Icon(
icon,
color: AppColors.primary,
size: 18.sp,
),
SizedBox(width: 12.w),
Expanded(
child: Text(
label,
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black,
),
),
),
Text(
value,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
],
);
}
}

168
lib/modules/translation/views/translation_view.dart

@ -109,7 +109,6 @@ class TranslationView extends GetView<TranslationController> {
)),
centerTitle: true,
actions: [
Obx(() {
if (controller.isRecording.value) {
// 录音中:显示红色计时器样式,点击可停止录音
@ -305,7 +304,8 @@ class TranslationView extends GetView<TranslationController> {
itemCount: itemCount,
itemBuilder: (context, index) {
// 如果是最后一项且有更多历史记录,显示加载指示器(reverse模式下)
if (index == controller.translationHistory.length && controller.hasMoreHistory.value) {
if (index == controller.translationHistory.length &&
controller.hasMoreHistory.value) {
return _buildLoadMoreIndicator();
}
@ -313,7 +313,8 @@ class TranslationView extends GetView<TranslationController> {
final historyIndex = controller.translationHistory.length - 1 - index;
// 检查索引是否有效(reverse模式下的索引验证)
if (historyIndex < 0 || historyIndex >= controller.translationHistory.length) {
if (historyIndex < 0 ||
historyIndex >= controller.translationHistory.length) {
return const SizedBox.shrink();
}
@ -670,32 +671,35 @@ class TranslationView extends GetView<TranslationController> {
void _showLanguageSelectionDialog(bool isSource, bool isDarkMode) {
Get.dialog(
Dialog(
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24.r),
borderRadius: BorderRadius.circular(16.r), // 统一圆角尺寸为16
),
insetPadding: EdgeInsets.symmetric(horizontal: 20.w),
backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white,
child: Container(
width: 0.9.sw,
padding: EdgeInsets.only(top: 32.h, bottom: 16.h),
child: Padding(
// 使用Padding替代Container的padding
padding: EdgeInsets.all(12.w), // 统一内边距
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题区域
Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
padding: EdgeInsets.symmetric(horizontal: 8.w),
child: Text(
isSource
? 'selectSourceLanguage'.tr // 选择源语言
: 'selectTargetLanguage'.tr, // 选择目标语言
? 'selectSourceLanguage'.tr
: 'selectTargetLanguage'.tr, // 保留源语言/目标语言区分
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black,
fontSize: 18.sp, // 调整字号
fontWeight: FontWeight.w600, // 使用数字字重
color: isDarkMode ? Colors.white : Colors.black87, // 优化文本颜色
),
),
),
SizedBox(height: 16.h),
// 语言列表区域
Container(
constraints: BoxConstraints(maxHeight: 0.5.sh),
child: SingleChildScrollView(
@ -703,52 +707,105 @@ class TranslationView extends GetView<TranslationController> {
mainAxisSize: MainAxisSize.min,
children:
controller.supportedLanguages.keys.map((language) {
return ListTile(
title: Text(
'${language}'.tr,
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black,
return Obx(() {
final currentLanguage = isSource
? controller.sourceLanguage.value
: controller.targetLanguage.value;
final isSelected = currentLanguage == language;
return GestureDetector(
// 改用手势检测器实现点击
onTap: () {
if (isSource) {
controller.setSourceLanguage(language);
} else {
controller.setTargetLanguage(language);
}
Get.back();
},
child: Container(
margin: EdgeInsets.only(bottom: 10.h), // 添加项间距
padding: EdgeInsets.all(10.w), // 统一内边距
decoration: BoxDecoration(
color: isSelected // 选中状态背景色
? (isDarkMode
? const Color(0xFF6200EE).withOpacity(0.2)
: Colors.blue.withOpacity(0.1))
: (isDarkMode // 未选中状态背景色
? Colors.white.withOpacity(0.05)
: Colors.grey[200]!),
borderRadius: BorderRadius.circular(12.r), // 项圆角
border: Border.all(
// 选中状态边框
color: isSelected
? (isDarkMode
? const Color(0xFF6200EE)
: Colors.blue)
: (isDarkMode // 未选中状态背景色
? Colors.white.withOpacity(0.05)
: Colors.grey[200]!),
width: 1,
),
),
child: Row(
children: [
// 语言名称
Expanded(
child: Text(
language.tr,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: isDarkMode
? Colors.white
: Colors.black87,
overflow: TextOverflow.ellipsis,
),
),
),
// 选中指示器
Container(
width: 24.w,
height: 24.w,
decoration: BoxDecoration(
color: isSelected // 动态颜色
? (isDarkMode
? const Color(0xFF6200EE)
: Colors.blue)
: (isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[300]),
shape: BoxShape.circle, // 圆形样式
),
child: isSelected
? Icon(
// 选中时显示对勾
Icons.check,
size: 16.w,
color: Colors.white,
)
: null,
),
],
),
),
),
trailing: Obx(() {
final currentLanguage = isSource
? controller.sourceLanguage.value
: controller.targetLanguage.value;
return currentLanguage == language
? Icon(
Icons.check_circle,
color: isDarkMode
? AppColors.primary
: Colors.blue,
)
: const SizedBox.shrink();
}),
onTap: () {
if (isSource) {
controller.setSourceLanguage(language);
} else {
controller.setTargetLanguage(language);
}
Get.back();
},
);
);
});
}).toList(),
),
),
),
SizedBox(height: 16.h),
// 取消按钮
Align(
alignment: Alignment.centerRight,
child: Padding(
padding: EdgeInsets.only(right: 24.w),
child: TextButton(
onPressed: () => Get.back(),
child: Text(
'cancel'.tr, // 取消
style: TextStyle(
color: isDarkMode ? AppColors.primary : Colors.blue,
fontSize: 16.sp,
),
child: TextButton(
onPressed: () => Get.back(),
child: Text(
'cancel'.tr,
style: TextStyle(
color: isDarkMode ? const Color(0xFF6200EE) : Colors.blue,
),
),
),
@ -802,7 +859,8 @@ class TranslationView extends GetView<TranslationController> {
child: GestureDetector(
onTap: () => controller.loadMoreHistory(),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.red[50],
borderRadius: BorderRadius.circular(20),
@ -979,7 +1037,7 @@ class TranslationView extends GetView<TranslationController> {
// 显示删除成功提示
Get.snackbar(
'删除成功', // deleteSuccess
'success'.tr, // deleteSuccess
'已删除该翻译记录', // deleteTranslationSuccess
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Get.isDarkMode ? Colors.grey[800] : Colors.grey[200],
@ -989,7 +1047,7 @@ class TranslationView extends GetView<TranslationController> {
} catch (e) {
// 显示删除失败提示
Get.snackbar(
'删除失败', // deleteFailed
'error'.tr, // deleteFailed
'删除翻译记录时发生错误: ${e.toString()}', // deleteTranslationError
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red[400],

9
lib/routes/app_pages.dart

@ -28,6 +28,8 @@ import '../modules/timbre/views/timbre_view.dart';
import '../modules/translation/views/translation_view.dart';
import '../modules/translation/bindings/translation_binding.dart';
import '../modules/translation/views/translation_history_view.dart';
import '../modules/translation/views/translation_stats_view.dart';
import '../modules/translation/bindings/translation_stats_binding.dart';
import '../modules/translation/bindings/translation_list_binding.dart';
import '../modules/translation/views/translation_list_view.dart';
import '../modules/translation/bindings/translation_history_binding.dart';
@ -148,6 +150,13 @@ abstract class AppPages {
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage(
name: Routes.translationStats,
page: () => const TranslationStatsView(),
binding: TranslationStatsBinding(),
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage(
name: Routes.translationList,
page: () => const TranslationListView(),

1
lib/routes/app_routes.dart

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

64
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt

@ -52,6 +52,9 @@ interface AgentServiceListener {
object AgentService : CoroutineScope {
private val TAG = "AgentService"
private var azureSpeechKey: String = ""
private var azureSpeechRegion: String = ""
// 协程相关
private val job = SupervisorJob()
override val coroutineContext: CoroutineContext
@ -179,6 +182,9 @@ object AgentService : CoroutineScope {
LocationService.initialize(appContext)
// 设置系统提示词
val customSystemPrompt = config["systemPrompt"] as? String
//保存配置
this.azureSpeechKey = config["azureSpeechKey"]?.toString() ?: ""
this.azureSpeechRegion = config["azureSpeechRegion"]?.toString() ?: ""
if (!customSystemPrompt.isNullOrBlank()) {
systemPrompt = customSystemPrompt
} else {
@ -188,9 +194,15 @@ object AgentService : CoroutineScope {
azureAsrHelper = AzureAsrHelper(context).apply {
// 使用协程处理异步初始化
CoroutineScope(Dispatchers.IO).launch {
//获取语音识别的语种
val supportedLanguagesConfig = config["supportedLanguages"] as? List<String>
val supportedLanguages = supportedLanguagesConfig?.toTypedArray()
?: arrayOf("zh-CN", "en-US") // 默认支持多语言
val asrInitSuccess = initialize(
subscriptionKey = config["azureSpeechKey"]?.toString() ?: "",
region = config["azureSpeechRegion"]?.toString() ?: "",
supportedLanguages = supportedLanguages,
audioSourceType = if (isExternalActive) AzureAsrHelper.AudioSourceType.EXTERNAL
else AzureAsrHelper.AudioSourceType.MICROPHONE
)
@ -1091,6 +1103,58 @@ object AgentService : CoroutineScope {
}
}
/**
* 设置支持的语音识别语言
*/
fun setSupportedLanguages(languages: List<String>): Boolean {
if (!isInitialized) {
Log.e(TAG, "服务未初始化")
sendEvent("error", mapOf("code" to "NOT_INITIALIZED", "message" to "服务未初始化"))
return false
}
if (languages.isEmpty()) {
Log.e(TAG, "语言列表不能为空")
sendEvent("error", mapOf("code" to "EMPTY_LANGUAGES", "message" to "语言列表不能为空"))
return false
}
return try {
Log.i(TAG, "设置支持的语言: ${languages.joinToString(", ")}")
// 重新初始化ASR服务以应用新的语言设置
val success = azureAsrHelper?.initialize(
subscriptionKey = this.azureSpeechKey,
region = this.azureSpeechRegion,
supportedLanguages = languages.toTypedArray(),
audioSourceType = audioSourceType
) ?: false
if (success) {
Log.i(TAG, "语言设置更新成功")
sendEvent("languages_updated", mapOf(
"languages" to languages,
"status" to "success"
))
} else {
Log.e(TAG, "语言设置更新失败")
sendEvent("error", mapOf(
"code" to "LANGUAGE_UPDATE_FAILED",
"message" to "语言设置更新失败"
))
}
success
} catch (e: Exception) {
Log.e(TAG, "设置支持语言异常: ${e.message}")
sendEvent("error", mapOf(
"code" to "LANGUAGE_UPDATE_ERROR",
"message" to e.message.toString()
))
false
}
}
/**
* 保存聊天消息
*/

16
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt

@ -201,6 +201,22 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
}
}
"setSupportedLanguages" -> {
val languages = call.argument<List<String>>("languages")
if (languages.isNullOrEmpty()) {
result.error("INVALID_ARGUMENTS", "语言列表不能为空", null)
return
}
try {
val isSuccess = AgentService.setSupportedLanguages(languages)
result.success(isSuccess)
} catch (e: Exception) {
Log.e(TAG, "设置支持语言失败", e)
result.error("SET_LANGUAGES_ERROR", "设置支持语言失败: ${e.message}", null)
}
}
"stopAiSteam" -> {
try {
AgentService.stopAiStream()

3
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift

@ -197,6 +197,9 @@ class AgentServiceImpl: NSObject {
private func initializeAzureSpeech() -> Bool {
azureTtsHelper?.addListener(self)
// 获取支持的语言列表,支持从配置中读取
let supportedLanguages = config["supportedLanguages"] as? [String] ?? ["zh-CN", "en-US"]
guard let asrSuccess = azureAsrHelper?.initialize(
subscriptionKey: azureSpeechKey,
region: azureSpeechRegion,

15
local_plugins/agent_service/lib/agent_service.dart

@ -332,6 +332,21 @@ class AgentService {
}
}
/// 设置支持的语音识别语言
///
/// [languages] 支持的语言列表,如 ['zh-CN', 'en-US']
/// 返回是否设置成功
static Future<bool> setSupportedLanguages(List<String> languages) async {
try {
final bool result = await _channel.invokeMethod('setSupportedLanguages', {
'languages': languages,
});
return result;
} on PlatformException catch (e) {
throw AgentServiceException(e.code, e.message ?? '设置支持语言失败', e.details);
}
}
/// 停止语音合成
static Future<bool> stopAiSteam() async {
try {

2
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt

@ -87,7 +87,7 @@ class AzureAsrHelper(private val context: Context) {
fun initialize(
subscriptionKey: String,
region: String,
supportedLanguages: Array<String> = arrayOf("zh-CN"),
supportedLanguages: Array<String> = arrayOf("zh-CN", "en-US"),
audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE
): Boolean {
try {

54
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt

@ -106,6 +106,15 @@ object BleService {
// 初始化状态
private var isInitialized = false
private val replyTimeoutHandler = Handler(Looper.getMainLooper())
private val replyTimeoutRunnable = Runnable {
if (!isReply) {
Log.w(TAG, "命令响应超时,自动重置 isReply 为 true")
isReply = true
processNextCommand()
}
}
/**
* 初始化BLE服务
* @param appContext 应用上下文
@ -471,6 +480,8 @@ object BleService {
} catch (e: Exception) {
Log.e(TAG, "关闭GATT连接异常: ${e.message}", e)
}
isReply = true;
commandQueue.clear()
bluetoothGatt = null
notifyChar = null
writeChar = null
@ -520,6 +531,7 @@ object BleService {
newState == BluetoothProfile.STATE_DISCONNECTED -> {
updateConnectionState(BleConst.STATE_DISCONNECTED)
disconnectGatt()
// otaManager?.onBtDeviceConnection(g.device, StateCode.CONNECTION_CONNECTING)
// g.close()
// otaManager?.release();
@ -752,19 +764,10 @@ object BleService {
}
}"
)
isReply = true;
cmdReplyType = cmdType;
if (!commandQueue.isEmpty()) {
val nextCmd = commandQueue.poll()
writeChar!!.value = nextCmd
var isSeccess = bluetoothGatt?.writeCharacteristic(writeChar)
if (isSeccess == true) {
Log.d(
TAG,
"收到设备响应:"
)
}
}
isReply = true
cmdReplyType = cmdType
replyTimeoutHandler.removeCallbacks(replyTimeoutRunnable) // 移除超时回调
processNextCommand()
// 根据命令类型处理不同的响应
when (cmdType) {
BleConst.CMD_GET_VERSION -> {
@ -1385,13 +1388,16 @@ object BleService {
// 加入队列并触发发送
if (isReply) {
var isSeccess = bluetoothGatt?.writeCharacteristic(writeChar)
if (isSeccess == true) {
isReply = false;
cmdType;
val isSuccess = bluetoothGatt?.writeCharacteristic(writeChar)
if (isSuccess == true) {
isReply = false
replyTimeoutHandler.postDelayed(replyTimeoutRunnable, 1000) // 设置1秒超时
return true
} else
{
commandQueue.poll()
return false
}
} else {
commandQueue.offer(cmdPacket)
}
@ -1403,6 +1409,19 @@ object BleService {
}
}
private fun processNextCommand() {
if (!commandQueue.isEmpty()) {
val nextCmd = commandQueue.poll()
writeChar!!.value = nextCmd
val isSuccess = bluetoothGatt?.writeCharacteristic(writeChar)
if (isSuccess == true) {
isReply = false
replyTimeoutHandler.postDelayed(replyTimeoutRunnable, 1000) // 设置1秒超时
Log.d(TAG, "发送队列中的下一个命令")
}
}
}
/**
* 计算CRC8/MAXIM校验值
*/
@ -1588,4 +1607,3 @@ class RecordingFile(private val context: Context) {
}
}
}
Loading…
Cancel
Save