Browse Source

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

newdev_shunjiawei
liwei1dao 11 months ago
parent
commit
73a8deca9a
  1. 14
      lib/data/models/goods.dart
  2. 115
      lib/data/models/meeting_usage_stats.dart
  3. 28
      lib/data/models/user_Info.dart
  4. 37
      lib/data/services/meeting/meeting_task_service.dart
  5. 32
      lib/data/services/network/api.dart
  6. 296
      lib/data/services/usage_stats_service.dart
  7. 6
      lib/modules/explore/controllers/explore_controller.dart
  8. 2
      lib/modules/explore/views/explore_view.dart
  9. 4
      lib/modules/goods/controllers/goodstrade_controller.dart
  10. 16
      lib/modules/translation/controllers/translation_controller.dart
  11. 13
      lib/modules/usage/bindings/translation_stats_binding.dart
  12. 20
      lib/modules/usage/bindings/usage_binding.dart
  13. 152
      lib/modules/usage/controllers/meeting_stats_controller.dart
  14. 15
      lib/modules/usage/controllers/translation_stats_controller.dart
  15. 20
      lib/modules/usage/controllers/usage_controller.dart
  16. 171
      lib/modules/usage/views/meeting_stats_view.dart
  17. 158
      lib/modules/usage/views/translation_stats_view.dart
  18. 50
      lib/modules/usage/views/usage_view.dart
  19. 10
      lib/routes/app_pages.dart
  20. 2
      lib/routes/app_routes.dart

14
lib/data/models/goods.dart

@ -6,7 +6,7 @@ enum UsageType {
/// 未知使用量类型
UsageNull, // 0
/// 翻译
Trade, // 1
Translation, // 1
/// 会议
Meet, // 2
/// 智能体
@ -19,7 +19,7 @@ enum UsageType {
case 0:
return UsageType.UsageNull;
case 1:
return UsageType.Trade;
return UsageType.Translation;
case 2:
return UsageType.Meet;
case 3:
@ -36,8 +36,8 @@ enum UsageType {
case 'null':
case 'unknown':
return UsageType.UsageNull;
case 'trade':
return UsageType.Trade;
case 'translation':
return UsageType.Translation;
case 'meet':
case 'meeting':
return UsageType.Meet;
@ -54,7 +54,7 @@ enum UsageType {
switch (this) {
case UsageType.UsageNull:
return 0;
case UsageType.Trade:
case UsageType.Translation:
return 1;
case UsageType.Meet:
return 2;
@ -67,8 +67,8 @@ enum UsageType {
switch (this) {
case UsageType.UsageNull:
return 'UsageNull';
case UsageType.Trade:
return 'Trade';
case UsageType.Translation:
return 'Translation';
case UsageType.Meet:
return 'Meet';
case UsageType.AI:

115
lib/data/models/meeting_usage_stats.dart

@ -0,0 +1,115 @@
///会议模块用量统计的模型类
const int asrStatusProcessing = 0; // 处理中
const int asrStatusFailed = 1; // 处理失败
const int asrStatusCompleted = 2; // 已完成
// 会议音频文件转文字单次数据
class MeetingAsrRecord {
String id; // 本次ASR唯一标识
int meetingId; // 会议记录ID
String taskId; // 云端任务ID
int audioSeconds; // 音频总时长(秒)
int fileSizeBytes; // 音频文件大小(字节)
String language; // ASR语言
int submitTimestamp; // 提交时间戳(毫秒)
int status; // 状态: 0-处理中, 1-处理失败, 2-已完成
int transcriptBytes; // 转写后的字节数(utf8)
int completeTimestamp; // 完成时间戳(毫秒)
MeetingAsrRecord({
required this.id,
required this.meetingId,
required this.taskId,
required this.audioSeconds,
required this.fileSizeBytes,
required this.language,
required this.submitTimestamp,
required this.status,
required this.transcriptBytes,
required this.completeTimestamp,
});
Map<String, dynamic> toJson() => {
'id': id,
'meetingId': meetingId,
'taskId': taskId,
'audioSeconds': audioSeconds,
'fileSizeBytes': fileSizeBytes,
'language': language,
'submitTimestamp': submitTimestamp,
'status': status,
'transcriptBytes': transcriptBytes,
'completeTimestamp': completeTimestamp,
};
factory MeetingAsrRecord.fromJson(Map<String, dynamic> json) {
return MeetingAsrRecord(
id: json['id'] ?? '',
meetingId: json['meetingId'] ?? 0,
taskId: json['taskId'] ?? '',
audioSeconds: json['audioSeconds'] ?? 0,
fileSizeBytes: json['fileSizeBytes'] ?? 0,
language: json['language'] ?? '',
submitTimestamp: json['submitTimestamp'] ?? 0,
status: json['status'] ?? 0,
transcriptBytes: json['transcriptBytes'] ?? 0,
completeTimestamp: json['completeTimestamp'] ?? 0,
);
}
}
// 会议音频文件转文字单日统计
class MeetingAsrDailyStats {
String date; // 日期字符串: YYYY-MM-DD
String userId; // 用户ID
List<MeetingAsrRecord> items; // 当天所有单次ASR
MeetingAsrDailyStats({
required this.date,
required this.userId,
required this.items,
});
// 计算型聚合,不冗余存储
int get asrCalls => items.length;
int get audioSecondsTotal => items.fold(0, (s, r) => s + r.audioSeconds);
int get audioBytesTotal => items.fold(0, (s, r) => s + r.fileSizeBytes);
int get transcriptBytesTotal =>
items.fold(0, (s, r) => s + r.transcriptBytes);
Map<String, dynamic> toJson() => {
'date': date,
'userId': userId,
'items': items.map((e) => e.toJson()).toList(),
};
factory MeetingAsrDailyStats.fromJson(Map<String, dynamic> json) {
final list = (json['items'] as List? ?? [])
.map((e) => MeetingAsrRecord.fromJson(Map<String, dynamic>.from(e)))
.toList();
return MeetingAsrDailyStats(
date: json['date'] ?? '',
userId: json['userId'] ?? 'anonymous',
items: list,
);
}
}
// 会议音频文件转文字历史统计(所有的用量)
class MeetingAsrHistoryStats {
String userId; // 用户ID
int allAsrCalls; // 所有ASR调用次数
int allAudioSeconds; // 所有音频总时长(秒)
int allAudioFileBytes; // 所有音频文件大小(字节)
int allTranscriptBytes; // 所有转写后的字节数(utf8)
MeetingAsrHistoryStats({
required this.userId,
required this.allAsrCalls,
required this.allAudioSeconds,
required this.allAudioFileBytes,
required this.allTranscriptBytes,
});
}

28
lib/data/models/user_Info.dart

@ -83,11 +83,9 @@ class User {
final devicesData = json['devices'] as List<dynamic>?; // 允许空值
//final token = json['data']['token'] as String? ?? '';
//final token = '';
// 更新单例实例
return _instance = User._(
final u = User._(
token: '',
//token: token as String? ?? '',
name: (userData?['name'] as String?) ?? '默认名称', // 提供默认值
name: (userData?['name'] as String?) ?? '默认名称',
uid: (userData?['uid'] as String?) ?? '',
avatar: (userData?['avatar'] as String?) ?? '',
language: (userData?['language'] as String?) ?? 'en',
@ -104,27 +102,29 @@ class User {
tradetotalintegral: (userData?['tradetotalintegral'] as int?) ?? 0,
meetintegral: (userData?['meetintegral'] as int?) ?? 0,
meettotalintegral: (userData?['meettotalintegral'] as int?) ?? 0,
devices: devicesData
?.map((e) => UserDevice.fromJson(e as Map<String, dynamic>))
.toList() ??
[], // 处理设备列表
[],
);
_instance = u;
Logger.i("User",
"初始化用户: uid=${u.uid}, name=${u.name}, viplv=${u.viplv}, vipexptime=${u.vipexptime}, ai聊天=${u.aichatintegral}/${u.aichattotalintegral}, 翻译=${u.tradeintegral}/${u.tradetotalintegral}, 会议=${u.meetintegral}/${u.meettotalintegral}");
return u;
}
factory User.fronTokenJson(Map<String, dynamic> json) {
// 修复1:安全获取嵌套的 user 字段
final userData = json['user'] as Map<String, dynamic>?; // 允许空值
final devicesData = json['devices'] as List<dynamic>?; // 允许空值
print("数据获取fronTokenJson$userData $devicesData");
// 更新单例实例
return _instance = User._(
Logger.d("User",
"fronTokenJson: user=$userData, devices=${devicesData?.length ?? 0}");
final u = User._(
token: '',
name: (userData?['name'] as String?) ?? '默认名称', // 提供默认值
name: (userData?['name'] as String?) ?? '默认名称',
uid: (userData?['uid'] as String?) ?? '',
avatar: (userData?['avatar'] as String?) ?? '',
language: (userData?['language'] as String?) ?? 'en',
mail: (userData?['mail'] as String?) ?? '',
phone: (userData?['phone'] as String?) ?? '',
phonemac: (userData?['phonemac'] as String?) ?? '',
@ -141,8 +141,12 @@ class User {
? (devicesData)
.map((e) => UserDevice.fromJson(e as Map<String, dynamic>))
.toList()
: [], // 处理设备列表
: [],
);
_instance = u;
Logger.i("User",
"初始化用户(token): uid=${u.uid}, name=${u.name}, viplv=${u.viplv}, vipexptime=${u.vipexptime}, ai聊天=${u.aichatintegral}/${u.aichattotalintegral}, 翻译=${u.tradeintegral}/${u.tradetotalintegral}, 会议=${u.meetintegral}/${u.meettotalintegral}");
return u;
}
updateUserInfo(Map<String, dynamic> user) {

37
lib/data/services/meeting/meeting_task_service.dart

@ -1,4 +1,5 @@
import 'dart:convert';
import 'dart:io';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
@ -9,6 +10,8 @@ import '../../../core/utils/synchrodata.dart';
import '../db/sqflite_api.dart';
import '../deapsound_ai_service.dart';
import 'meeting_upload_service.dart';
import '../usage_stats_service.dart';
import '../../models/meeting_usage_stats.dart';
///会议任务服务类,负责管理会议音频的转写任务处理流程,包括任务提交、状态轮询、结果处理和AI分析等功能。
class MeetingTaskService extends GetxService {
@ -117,6 +120,30 @@ class MeetingTaskService extends GetxService {
taskId = await _submitTask(audiourl, isSpeaker, language);
}
await SqfliteApi.editMeeting(id, {'tasktype': 1, 'taskid': taskId});
try {
final detail = await SqfliteApi.getMeetingDetails(id);
final seconds = (detail['seconds'] ?? 0) as int;
final filepath = (detail['filepath'] ?? '') as String;
int fileSizeBytes = 0;
if (filepath.isNotEmpty) {
try {
fileSizeBytes = await File(filepath).length();
} catch (_) {}
}
final record = MeetingAsrRecord(
id: taskId,
meetingId: id,
taskId: taskId,
audioSeconds: seconds,
fileSizeBytes: fileSizeBytes,
language: language,
submitTimestamp: DateTime.now().millisecondsSinceEpoch,
status: asrStatusProcessing,
transcriptBytes: 0,
completeTimestamp: 0,
);
Get.find<UsageStatsService>().addMeetingAsrRecord(record);
} catch (_) {}
_taskList.add({
'id': id,
'taskid': taskId,
@ -373,6 +400,16 @@ class MeetingTaskService extends GetxService {
_storage.write("meeting_task_list", _taskList);
_storage.write("meeting_task_finish_list", taskFinishList);
}
try {
final details = await SqfliteApi.getMeetingDetails(id);
final tid = (details['taskid'] ?? '') as String;
final transcriptBytes = utf8.encode(text).length;
Get.find<UsageStatsService>().completeMeetingAsrRecord(
id: tid,
transcriptBytes: transcriptBytes,
completeTimestamp: DateTime.now().millisecondsSinceEpoch,
);
} catch (_) {}
// 触发AI分析,分别请求总结和概览
aiServiceMessage(id, text, prompt, outline);

32
lib/data/services/network/api.dart

@ -235,7 +235,37 @@ class Api {
);
}
//资源使用请求
/// 资源使用上报(`/api/home/user_usages`)
///
/// 用途:在 AI 聊天等场景中,上报本次会话的消耗与功能调用次数,服务端据此统计并更新积分。
///
/// 请求方式:`POST /api/home/user_usages`
///
/// 认证:已通过全局拦截器自动携带 `Authorization`(见 `AuthInterceptor`)。
///
/// 参数(`params`)结构:
/// - `usagetype`:`int` 类型,使用场景类型;Android 端 Agent 使用 `3` 表示 AICHAT。
/// - `usages`:`Map<String, int>`,各项指标的键值对,例如:
/// - `AICHAT_PROMPT_TOKENS`:本次对话的提示词 Token 数
/// - `AICHAT_COMPLETION_TOKENS`:本次对话的生成 Token 数
/// - `AICHAT_FUNCTION_{函数名}`:被调用的函数次数(按函数名累加)
///
/// 返回:标准封装后 `code == 0` 时返回 `data`。其中 `data.usages` 可能包含键 `"3"`,表示最新的 AI 积分值。
///
/// 使用示例:
/// ```dart
/// final params = {
/// 'usagetype': 3, // AICHAT
/// 'usages': {
/// 'AICHAT_PROMPT_TOKENS': 120,
/// 'AICHAT_COMPLETION_TOKENS': 256,
/// 'AICHAT_FUNCTION_toolX': 1,
/// }
/// };
/// final data = await Api.usagesResPoints(params);
/// final usages = (data['usages'] as Map?)?.cast<String, dynamic>();
/// final aiIntegral = usages?['3']; // 服务端返回的最新 AI 积分(如有)
/// ```
static usagesResPoints([params]) {
return DioManager().request(
NWMethod.post,

296
lib/data/services/usage_stats_service.dart

@ -1,21 +1,26 @@
import 'dart:convert';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import '../models/translation_usage_stats.dart';
import '../models/meeting_usage_stats.dart';
import '../../core/utils/logger.dart';
import 'network/api.dart';
import '../models/goods.dart';
import '../models/user_Info.dart';
/// 使用量统计服务
///
class UsageStatsService extends GetxService {
static const String _storageKey = 'usage_stats';
static const String _dailyStatsKey = 'daily_translation_stats';
static const String _meetingDailyKey = 'daily_meeting_asr_stats';
final GetStorage _storage = GetStorage();
// 实时统计数据
final Rx<RealtimeUsageStats> realtimeStats = RealtimeUsageStats().obs;
final Rx<RealtimeUsageStats> realtimeTranslationStats =
RealtimeUsageStats().obs;
// 当日统计数据
final Rx<TranslationUsageStats> todayStats =
final Rx<TranslationUsageStats> todayTranslationStats =
TranslationUsageStats.empty('').obs;
@override
@ -26,12 +31,12 @@ class UsageStatsService extends GetxService {
/// 初始化服务
void _initializeService() {
_loadTodayStats();
_loadTodayTranslationStats();
Logger.info('lxm--使用量统计服务初始化完成');
}
/// 加载当日统计数据
void _loadTodayStats() {
/// 加载当日翻译统计数据
void _loadTodayTranslationStats() {
try {
final today = DateTime.now();
final todayKey =
@ -39,14 +44,16 @@ class UsageStatsService extends GetxService {
final statsData = _storage.read('${_dailyStatsKey}_$todayKey');
if (statsData != null) {
todayStats.value = TranslationUsageStats.fromJson(
todayTranslationStats.value = TranslationUsageStats.fromJson(
Map<String, dynamic>.from(statsData));
} else {
todayStats.value = TranslationUsageStats.empty(getCurrentUserId());
todayTranslationStats.value =
TranslationUsageStats.empty(getCurrentUserId());
}
} catch (e) {
Logger.error('加载当日统计数据失败: $e');
todayStats.value = TranslationUsageStats.empty(getCurrentUserId());
Logger.error('加载当日翻译统计数据失败: $e');
todayTranslationStats.value =
TranslationUsageStats.empty(getCurrentUserId());
}
}
@ -63,6 +70,69 @@ class UsageStatsService extends GetxService {
return 'anonymous';
}
MeetingAsrDailyStats _loadMeetingDaily(String dateKey) {
final data = _storage.read('${_meetingDailyKey}_$dateKey');
if (data != null) {
return MeetingAsrDailyStats.fromJson(Map<String, dynamic>.from(data));
}
return MeetingAsrDailyStats(
date: dateKey, userId: getCurrentUserId(), items: []);
}
void _saveMeetingDaily(MeetingAsrDailyStats stats) {
_storage.write('${_meetingDailyKey}_${stats.date}', stats.toJson());
}
String _formatDate(DateTime d) {
final y = d.year;
final m = d.month.toString().padLeft(2, '0');
final day = d.day.toString().padLeft(2, '0');
return '$y-$m-$day';
}
void addMeetingAsrRecord(MeetingAsrRecord record) {
final dateKey = _formatDate(
DateTime.fromMillisecondsSinceEpoch(record.submitTimestamp));
final daily = _loadMeetingDaily(dateKey);
final idx = daily.items.indexWhere((e) => e.id == record.id);
if (idx >= 0) {
daily.items[idx] = record;
} else {
daily.items.add(record);
}
_saveMeetingDaily(daily);
}
void completeMeetingAsrRecord({
required String id,
required int transcriptBytes,
required int completeTimestamp,
}) {
final now = DateTime.now();
for (int i = 0; i < 3; i++) {
final dateKey = _formatDate(now.subtract(Duration(days: i)));
final daily = _loadMeetingDaily(dateKey);
final idx = daily.items.indexWhere((e) => e.id == id);
if (idx >= 0) {
final old = daily.items[idx];
daily.items[idx] = MeetingAsrRecord(
id: old.id,
meetingId: old.meetingId,
taskId: old.taskId,
audioSeconds: old.audioSeconds,
fileSizeBytes: old.fileSizeBytes,
language: old.language,
submitTimestamp: old.submitTimestamp,
status: asrStatusCompleted,
transcriptBytes: transcriptBytes,
completeTimestamp: completeTimestamp,
);
_saveMeetingDaily(daily);
return;
}
}
}
/// 记录翻译API调用
void recordTranslationApiCall({
required String sourceText,
@ -73,20 +143,20 @@ class UsageStatsService extends GetxService {
}) {
try {
// 更新实时统计
realtimeStats.value.translationCount++;
realtimeStats.value.sourceTextCharacters += sourceText.length;
realtimeStats.value.ttsTextCharacters += targetText.length;
realtimeStats.value.currentLanguagePair =
realtimeTranslationStats.value.translationCount++;
realtimeTranslationStats.value.sourceTextCharacters += sourceText.length;
realtimeTranslationStats.value.ttsTextCharacters += targetText.length;
realtimeTranslationStats.value.currentLanguagePair =
'$sourceLanguage-$targetLanguage';
realtimeStats.value.currentMode = mode;
realtimeStats.refresh();
realtimeTranslationStats.value.currentMode = mode;
realtimeTranslationStats.refresh();
// 更新当日统计 - 使用多维度统计
final languagePair = '$sourceLanguage→$targetLanguage';
// 更新语言对统计
final newLanguagePairStats = Map<String, LanguagePairStats>.from(
todayStats.value.languagePairStats);
todayTranslationStats.value.languagePairStats);
if (newLanguagePairStats.containsKey(languagePair)) {
final existing = newLanguagePairStats[languagePair]!;
newLanguagePairStats[languagePair] = LanguagePairStats(
@ -103,8 +173,8 @@ class UsageStatsService extends GetxService {
}
// 更新模式统计
final newModeStats =
Map<String, TranslationModeStats>.from(todayStats.value.modeStats);
final newModeStats = Map<String, TranslationModeStats>.from(
todayTranslationStats.value.modeStats);
if (newModeStats.containsKey(mode)) {
final existing = newModeStats[mode]!;
newModeStats[mode] = existing.copyWith(
@ -120,12 +190,13 @@ class UsageStatsService extends GetxService {
);
}
todayStats.value = todayStats.value.copyWith(
translationApiCalls: todayStats.value.translationApiCalls + 1,
sourceTextCharacters:
todayStats.value.sourceTextCharacters + sourceText.length,
todayTranslationStats.value = todayTranslationStats.value.copyWith(
translationApiCalls:
todayTranslationStats.value.translationApiCalls + 1,
sourceTextCharacters: todayTranslationStats.value.sourceTextCharacters +
sourceText.length,
ttsTextCharacters:
todayStats.value.ttsTextCharacters + targetText.length,
todayTranslationStats.value.ttsTextCharacters + targetText.length,
languagePairStats: newLanguagePairStats,
modeStats: newModeStats,
);
@ -150,13 +221,15 @@ class UsageStatsService extends GetxService {
}) {
try {
// 更新实时统计(使用有效语音时长作为主要计费依据)
realtimeStats.value.effectiveAudioDuration += effectiveDurationSeconds;
realtimeStats.value.totalSessionDuration += totalDurationSeconds;
realtimeStats.refresh();
realtimeTranslationStats.value.effectiveAudioDuration +=
effectiveDurationSeconds;
realtimeTranslationStats.value.totalSessionDuration +=
totalDurationSeconds;
realtimeTranslationStats.refresh();
// 更新模式统计
final newModeStats =
Map<String, TranslationModeStats>.from(todayStats.value.modeStats);
final newModeStats = Map<String, TranslationModeStats>.from(
todayTranslationStats.value.modeStats);
if (newModeStats.containsKey(mode)) {
final existing = newModeStats[mode]!;
newModeStats[mode] = existing.copyWith(
@ -170,12 +243,13 @@ class UsageStatsService extends GetxService {
}
// 更新当日统计
todayStats.value = todayStats.value.copyWith(
asrApiCalls: todayStats.value.asrApiCalls + 1,
todayTranslationStats.value = todayTranslationStats.value.copyWith(
asrApiCalls: todayTranslationStats.value.asrApiCalls + 1,
effectiveAudioDuration:
todayStats.value.effectiveAudioDuration + effectiveDurationSeconds,
totalSessionDuration:
todayStats.value.totalSessionDuration + totalDurationSeconds,
todayTranslationStats.value.effectiveAudioDuration +
effectiveDurationSeconds,
totalSessionDuration: todayTranslationStats.value.totalSessionDuration +
totalDurationSeconds,
modeStats: newModeStats,
);
@ -186,6 +260,89 @@ class UsageStatsService extends GetxService {
} catch (e) {
Logger.error('记录ASR API调用失败: $e');
}
_reportAsrUsageToServer(
effectiveDurationSeconds: effectiveDurationSeconds,
//totalDurationSeconds: totalDurationSeconds,
language: language,
mode: mode,
);
}
/// 上报ASR用量到服务器
Future<void> _reportAsrUsageToServer({
required int effectiveDurationSeconds,
//required int totalDurationSeconds,
required String language,
required String mode,
}) async {
try {
final params = {
'usagetype': UsageType.Translation.toInt,
'usages': {
// 'TRANSLATION_ASR_EFFECTIVE_SECONDS': effectiveDurationSeconds,
//'TRANSLATION_ASR_TOTAL_SECONDS': totalDurationSeconds,
'TRANSLATION_MODE_$mode': effectiveDurationSeconds,
'TRANSLATION_ASR_LANGUAGE_$language': 1,
'TRANSLATION_SOURCE_CHARACTERS':
realtimeTranslationStats.value.sourceTextCharacters,
'TRANSLATION_TARGET_CHARACTERS':
realtimeTranslationStats.value.ttsTextCharacters,
},
};
Logger.info('lxm---ASR用量上报参数: $params');
final response = await Api.usagesResPoints(params);
Logger.info('lxm---ASR用量上报响应: $response');
_applyUsageResponse(response);
} catch (e) {
Logger.error('记录ASR API调用失败: $e');
}
}
/// 根据服务器用量上报回应,更新用户积分(非总积分)
/// 用量类型:0 AI聊天、1 翻译、2 会议
void _applyUsageResponse(dynamic response) {
try {
if (response is! Map) return;
final usagesObj = response['usages'];
if (usagesObj is! Map) return;
final usages = Map<String, dynamic>.from(usagesObj as Map);
int _asInt(dynamic v) {
if (v == null) return 0;
if (v is int) return v;
if (v is num) return v.toInt();
if (v is String) return int.tryParse(v) ?? 0;
return 0;
}
final Map<String, dynamic> update = {};
usages.forEach((k, v) {
final key = int.tryParse(k.toString());
final val = _asInt(v);
if (key == null) return;
switch (key) {
case 0:
update['aichatintegral'] = val;
break;
case 1:
update['tradeintegral'] = val;
break;
case 2:
update['meetintegral'] = val;
break;
}
});
if (update.isNotEmpty) {
User.instance.updateUserInfo(update);
Logger.info('积分更新: $update');
}
} catch (e) {
Logger.error('解析用量回应并更新积分失败: $e');
}
}
/// 记录TTS API调用
@ -196,15 +353,16 @@ class UsageStatsService extends GetxService {
}) {
try {
// 更新实时统计
realtimeStats.value.totalSessionDuration += durationSeconds;
realtimeStats.refresh();
realtimeTranslationStats.value.totalSessionDuration += durationSeconds;
realtimeTranslationStats.refresh();
// 更新当日统计(TTS按字符数计费,不是按时长)
todayStats.value = todayStats.value.copyWith(
ttsApiCalls: todayStats.value.ttsApiCalls + 1,
ttsTextCharacters: todayStats.value.ttsTextCharacters + text.length,
todayTranslationStats.value = todayTranslationStats.value.copyWith(
ttsApiCalls: todayTranslationStats.value.ttsApiCalls + 1,
ttsTextCharacters:
todayTranslationStats.value.ttsTextCharacters + text.length,
totalSessionDuration:
todayStats.value.totalSessionDuration + durationSeconds,
todayTranslationStats.value.totalSessionDuration + durationSeconds,
);
_saveTodayStats();
@ -219,8 +377,8 @@ class UsageStatsService extends GetxService {
void recordAiSummaryApiCall() {
try {
// 更新当日统计
todayStats.value = todayStats.value.copyWith(
aiSummaryApiCalls: todayStats.value.aiSummaryApiCalls + 1,
todayTranslationStats.value = todayTranslationStats.value.copyWith(
aiSummaryApiCalls: todayTranslationStats.value.aiSummaryApiCalls + 1,
);
_saveTodayStats();
@ -233,11 +391,11 @@ class UsageStatsService extends GetxService {
/// 开始新会话
void startNewSession() {
realtimeStats.value.reset();
realtimeTranslationStats.value.reset();
// 更新当日会话数
todayStats.value = todayStats.value.copyWith(
sessionCount: todayStats.value.sessionCount + 1,
todayTranslationStats.value = todayTranslationStats.value.copyWith(
sessionCount: todayTranslationStats.value.sessionCount + 1,
);
_saveTodayStats();
@ -250,7 +408,8 @@ class UsageStatsService extends GetxService {
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());
_storage.write(
'${_dailyStatsKey}_$todayKey', todayTranslationStats.value.toJson());
} catch (e) {
Logger.error('保存当日统计数据失败: $e');
}
@ -384,4 +543,47 @@ class UsageStatsService extends GetxService {
return TranslationUsageStats.empty(getCurrentUserId());
}
}
Future<List<MeetingAsrDailyStats>> getMeetingHistoryStats({
required DateTime startDate,
required DateTime endDate,
}) async {
try {
final List<MeetingAsrDailyStats> list = [];
for (DateTime date = startDate;
date.isBefore(endDate) || date.isAtSameMomentAs(endDate);
date = date.add(const Duration(days: 1))) {
final key = _formatDate(date);
final daily = _loadMeetingDaily(key);
if (daily.items.isNotEmpty) {
list.add(daily);
}
}
return list;
} catch (e) {
Logger.error('获取会议历史统计失败: $e');
return [];
}
}
MeetingAsrHistoryStats aggregateMeetingStats(
List<MeetingAsrDailyStats> daily) {
int calls = 0;
int audioSeconds = 0;
int audioBytes = 0;
int transcriptBytes = 0;
for (final d in daily) {
calls += d.asrCalls;
audioSeconds += d.audioSecondsTotal;
audioBytes += d.audioBytesTotal;
transcriptBytes += d.transcriptBytesTotal;
}
return MeetingAsrHistoryStats(
userId: getCurrentUserId(),
allAsrCalls: calls,
allAudioSeconds: audioSeconds,
allAudioFileBytes: audioBytes,
allTranscriptBytes: transcriptBytes,
);
}
}

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

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

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

@ -120,7 +120,7 @@ class ExploreView extends GetView<ExploreController> {
color: isDarkMode ? Colors.white : Colors.grey[700],
),
padding: EdgeInsets.zero,
onPressed: controller.goToTranslationStats,
onPressed: controller.goToUsageStats,
),
),
],

4
lib/modules/goods/controllers/goodstrade_controller.dart

@ -7,7 +7,7 @@ import 'goods_base_controller.dart';
class GoodstradeController extends GoodsBaseController {
@override
Future<List<DBGoods>> fetchProducts() async {
final resp = await Api.getGoods({'usagetype': UsageType.Trade.toInt});
final resp = await Api.getGoods({'usagetype': UsageType.Translation.toInt});
if (resp == null || resp is! Map) return [];
if (resp.containsKey('error') || resp.containsKey('msg')) {
final msg = resp['error'] ?? resp['msg'] ?? '服务器错误';
@ -21,7 +21,7 @@ class GoodstradeController extends GoodsBaseController {
if (data is List) {
final list =
data.whereType<Map<String, dynamic>>().map(DBGoods.fromJson).toList();
return list.where((g) => g.usagetype == UsageType.Trade).toList();
return list.where((g) => g.usagetype == UsageType.Translation).toList();
}
return [];
}

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

@ -318,6 +318,9 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
/// [speakerId] 说话者ID
Future<void> _startFaceToFaceRecognition(int speakerId) async {
try {
if (_asrSessionStartTime == null) {
await _initializeSession();
}
await _asrService.startContinuousRecognition(_audioSourceType); //开启识别
_startAsrActiveTracking(); //开启识别活动跟踪
if (isRecording.value) {
@ -347,6 +350,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
}
await _asrService.stopContinuousRecognition();
_stopAsrActiveTracking(); //停止识别活动跟踪
_recordAsrUsageStats(sourceLanguage.value);
isRecognizing.value = false;
currentSessionId = null;
@ -902,6 +906,13 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
await _astService.stopContinuousTranslation();
}
_stopAsrActiveTracking();
final langName = _languageManager
.getChineseNameByAsrCode(
(detectedLanguageCode.isNotEmpty
? detectedLanguageCode
: sourceLanguageCode.value)) ??
'未知';
_recordAsrUsageStats(langName);
isRecognizing.value = false;
currentSessionId = null;
} catch (e) {
@ -1197,7 +1208,6 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
Logger.info(
'翻译统计: 源文本${sourceText.length}字符, 模式${currentMode.value}, 语言对$sourceLanguageName→$targetLanguageName');
_recordAsrUsageStats(sourceLanguageName);
} catch (e) {
Logger.error('记录翻译统计失败: $e');
}
@ -1579,6 +1589,10 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
Logger.info(
'ASR会话统计: 总时长${totalDurationSeconds}秒, 实际活跃时长${effectiveDurationSeconds}秒, 语言$language');
_asrSessionStartTime = null;
_asrActiveDuration = Duration.zero;
_isAsrActive = false;
_asrActiveStartTime = null;
} catch (e) {
Logger.error('记录ASR统计失败: $e');
}

13
lib/modules/usage/bindings/translation_stats_binding.dart

@ -1,13 +0,0 @@
import 'package:get/get.dart';
import '../controllers/translation_stats_controller.dart';
import '../../../data/services/usage_stats_service.dart';
class TranslationStatsBinding implements Bindings {
@override
void dependencies() {
if (!Get.isRegistered<UsageStatsService>()) {
Get.put(UsageStatsService(), permanent: true);
}
Get.lazyPut<TranslationStatsController>(() => TranslationStatsController());
}
}

20
lib/modules/usage/bindings/usage_binding.dart

@ -0,0 +1,20 @@
import 'package:get/get.dart';
import '../controllers/usage_controller.dart';
import '../controllers/meeting_stats_controller.dart';
import '../controllers/translation_stats_controller.dart';
import '../../../data/services/usage_stats_service.dart';
class UsageBinding implements Bindings {
@override
void dependencies() {
if (!Get.isRegistered<UsageStatsService>()) {
Get.put(UsageStatsService(), permanent: true);
}
if (!Get.isRegistered<TranslationStatsController>()) {
Get.lazyPut<TranslationStatsController>(() => TranslationStatsController());
}
Get.lazyPut<MeetingStatsController>(() => MeetingStatsController());
Get.lazyPut<UsageController>(() => UsageController());
}
}

152
lib/modules/usage/controllers/meeting_stats_controller.dart

@ -0,0 +1,152 @@
import 'package:get/get.dart';
import '../../../data/services/usage_stats_service.dart';
import '../../../data/models/meeting_usage_stats.dart';
import '../../../core/utils/logger.dart';
class MeetingStatsController extends GetxController {
final UsageStatsService _usageService = Get.find<UsageStatsService>();
final isLoading = false.obs;
final selectedTabIndex = 0.obs;
final selectedTimeRange = '今日'.obs;
final Rx<MeetingAsrHistoryStats> todayStats =
MeetingAsrHistoryStats(
userId: '',
allAsrCalls: 0,
allAudioSeconds: 0,
allAudioFileBytes: 0,
allTranscriptBytes: 0,
).obs;
final Rx<MeetingAsrHistoryStats> weekStats =
MeetingAsrHistoryStats(
userId: '',
allAsrCalls: 0,
allAudioSeconds: 0,
allAudioFileBytes: 0,
allTranscriptBytes: 0,
).obs;
final Rx<MeetingAsrHistoryStats> monthStats =
MeetingAsrHistoryStats(
userId: '',
allAsrCalls: 0,
allAudioSeconds: 0,
allAudioFileBytes: 0,
allTranscriptBytes: 0,
).obs;
final RxList<MeetingAsrDailyStats> historyDaily = <MeetingAsrDailyStats>[].obs;
@override
void onInit() {
super.onInit();
loadAllStats();
}
Future<void> loadAllStats() async {
isLoading.value = true;
try {
final now = DateTime.now();
final todayList = await _usageService.getMeetingHistoryStats(
startDate: DateTime(now.year, now.month, now.day),
endDate: DateTime(now.year, now.month, now.day),
);
todayStats.value = _usageService.aggregateMeetingStats(todayList);
final weekStart = now.subtract(Duration(days: now.weekday - 1));
final weekEnd = weekStart.add(const Duration(days: 6));
final weekList = await _usageService.getMeetingHistoryStats(
startDate: DateTime(weekStart.year, weekStart.month, weekStart.day),
endDate: DateTime(weekEnd.year, weekEnd.month, weekEnd.day),
);
weekStats.value = _usageService.aggregateMeetingStats(weekList);
final monthStart = DateTime(now.year, now.month, 1);
final monthEnd = DateTime(now.year, now.month + 1, 0);
final monthList = await _usageService.getMeetingHistoryStats(
startDate: monthStart,
endDate: monthEnd,
);
monthStats.value = _usageService.aggregateMeetingStats(monthList);
final historyStart = now.subtract(const Duration(days: 7));
historyDaily.value = await _usageService.getMeetingHistoryStats(
startDate: DateTime(historyStart.year, historyStart.month, historyStart.day),
endDate: DateTime(now.year, now.month, now.day),
);
Logger.info('会议统计数据加载完成');
} catch (e) {
Logger.error('加载会议统计数据失败: $e');
} finally {
isLoading.value = false;
}
}
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;
}
}
MeetingAsrHistoryStats get currentStats {
switch (selectedTabIndex.value) {
case 0:
return todayStats.value;
case 1:
return weekStats.value;
case 2:
return monthStats.value;
default:
return todayStats.value;
}
}
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 formatBytes(int bytes) {
if (bytes < 1024) return '$bytes B';
final kb = bytes / 1024;
if (kb < 1024) return '${kb.toStringAsFixed(1)} KB';
final mb = kb / 1024;
if (mb < 1024) return '${mb.toStringAsFixed(1)} MB';
final gb = mb / 1024;
return '${gb.toStringAsFixed(1)} GB';
}
String formatNumber(int number) {
if (number < 1000) return number.toString();
if (number < 1000000) return '${(number / 1000).toStringAsFixed(1)}K';
return '${(number / 1000000).toStringAsFixed(1)}M';
}
Future<void> refreshData() async {
await loadAllStats();
}
}

15
lib/modules/usage/controllers/translation_stats_controller.dart

@ -19,7 +19,8 @@ class TranslationStatsController extends GetxController {
final RxList<TranslationUsageStats> historyStats =
<TranslationUsageStats>[].obs;
RealtimeUsageStats get realtimeStats => _usageService.realtimeStats.value;
RealtimeUsageStats get realtimeStats =>
_usageService.realtimeTranslationStats.value;
final RxMap<String, double> costConfig = <String, double>{}.obs;
final RxDouble dailyCostThreshold = 10.0.obs;
@ -34,17 +35,11 @@ class TranslationStatsController extends GetxController {
}
void _initializeData() {
ever(_usageService.todayStats, (stats) {
ever(_usageService.todayTranslationStats, (stats) {
todayStats.value = stats;
_updateChartData();
});
loadAllStats();
}
@ -52,7 +47,7 @@ class TranslationStatsController extends GetxController {
isLoading.value = true;
try {
todayStats.value = _usageService.todayStats.value;
todayStats.value = _usageService.todayTranslationStats.value;
await _loadWeekStats();
@ -136,7 +131,6 @@ class TranslationStatsController extends GetxController {
int totalTtsPlayDuration = 0;
int totalSessions = 0;
final Map<String, int> combinedLanguagePairUsage = {};
final Map<String, int> combinedModeUsage = {};
@ -151,7 +145,6 @@ class TranslationStatsController extends GetxController {
totalTtsPlayDuration += stats.totalSessionDuration;
totalSessions += stats.sessionCount;
stats.languagePairUsage.forEach((key, value) {
combinedLanguagePairUsage[key] =
(combinedLanguagePairUsage[key] ?? 0) + value;

20
lib/modules/usage/controllers/usage_controller.dart

@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../views/translation_stats_view.dart';
import '../views/meeting_stats_view.dart';
class UsageController extends GetxController {
late PageController pageController;
final RxInt pageIndex = 0.obs;
final List<Widget> pageList = const [
TranslationStatsView(),
MeetingStatsView(),
];
@override
void onInit() {
super.onInit();
pageController = PageController(initialPage: 0);
}
}

171
lib/modules/usage/views/meeting_stats_view.dart

@ -0,0 +1,171 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../../../core/theme/app_colors.dart';
import '../controllers/meeting_stats_controller.dart';
class MeetingStatsView extends GetView<MeetingStatsController> {
const MeetingStatsView({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
? const Center(child: CircularProgressIndicator())
: _buildBody(isDarkMode)),
);
}
PreferredSizeWidget _buildAppBar(bool isDarkMode) {
return AppBar(
leading: IconButton(
icon: Icon(Icons.arrow_back_ios, color: isDarkMode ? Colors.white : Colors.black, size: 20.sp),
onPressed: () => Get.back(),
),
backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.white,
elevation: 0.5,
title: Text(
'会议统计',
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,
),
],
);
}
Widget _buildBody(bool isDarkMode) {
return Column(
children: [
_buildTimeRangeSelector(isDarkMode),
Expanded(
child: SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: Column(
children: [
_buildOverviewCard(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.withValues(alpha: 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 ? 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 _buildOverviewCard(bool isDarkMode) {
final stats = controller.currentStats;
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.withValues(alpha: 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),
Row(
children: [
Expanded(child: _buildStatItem('ASR调用次数', controller.formatNumber(stats.allAsrCalls), Icons.api, isDarkMode)),
Expanded(child: _buildStatItem('音频总时长', controller.formatDuration(stats.allAudioSeconds), Icons.mic, isDarkMode)),
],
),
SizedBox(height: 12.h),
Row(
children: [
Expanded(child: _buildStatItem('音频文件大小', controller.formatBytes(stats.allAudioFileBytes), Icons.folder, isDarkMode)),
Expanded(child: _buildStatItem('转写字节', controller.formatNumber(stats.allTranscriptBytes), 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),
),
],
);
}
}

158
lib/modules/usage/views/translation_stats_view.dart

@ -3,6 +3,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../controllers/translation_stats_controller.dart';
import '../../../core/theme/app_colors.dart';
import '../../../data/models/user_Info.dart';
class TranslationStatsView extends GetView<TranslationStatsController> {
const TranslationStatsView({Key? key}) : super(key: key);
@ -49,32 +50,32 @@ class TranslationStatsView extends GetView<TranslationStatsController> {
),
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('清理过期数据'),
),
PopupMenuItem(
value: 'export',
child: Text('导出数据'),
),
],
),
// 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('清理过期数据'),
// ),
// // PopupMenuItem(
// // value: 'export',
// // child: Text('导出数据'),
// // ),
// ],
// ),
],
);
}
@ -99,6 +100,8 @@ class TranslationStatsView extends GetView<TranslationStatsController> {
SizedBox(height: 16.h),
_buildOverviewCard(isDarkMode),
SizedBox(height: 16.h),
_buildIntegralCard(isDarkMode),
SizedBox(height: 16.h),
_buildApiCallsCard(isDarkMode),
SizedBox(height: 16.h),
_buildDataVolumeCard(isDarkMode),
@ -300,7 +303,6 @@ class TranslationStatsView extends GetView<TranslationStatsController> {
isDarkMode,
),
),
],
)),
SizedBox(height: 12.h),
@ -422,6 +424,106 @@ class TranslationStatsView extends GetView<TranslationStatsController> {
);
}
Widget _buildIntegralCard(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.withValues(alpha: 0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.stacked_line_chart,
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(() {
final _ = User.refreshTick.value;
final u = User.instance;
return Column(
children: [
_buildIntegralRow(
'AI聊天',
'${controller.formatNumber(u.aichatintegral)}/${controller.formatNumber(u.aichattotalintegral)}',
Icons.chat_bubble_outline,
isDarkMode,
),
SizedBox(height: 12.h),
_buildIntegralRow(
'翻译',
'${controller.formatNumber(u.tradeintegral)}/${controller.formatNumber(u.tradetotalintegral)}',
Icons.translate,
isDarkMode,
),
SizedBox(height: 12.h),
_buildIntegralRow(
'会议',
'${controller.formatNumber(u.meetintegral)}/${controller.formatNumber(u.meettotalintegral)}',
Icons.groups,
isDarkMode,
),
],
);
}),
],
),
);
}
Widget _buildIntegralRow(
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,
),
),
],
);
}
Widget _buildApiCallRow(
String label, int count, IconData icon, bool isDarkMode) {
return Row(
@ -747,4 +849,4 @@ class TranslationStatsView extends GetView<TranslationStatsController> {
],
);
}
}
}

50
lib/modules/usage/views/usage_view.dart

@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../../../core/theme/app_colors.dart';
import '../controllers/usage_controller.dart';
class UsageView extends GetView<UsageController> {
const UsageView({super.key});
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.grey[50],
body: PageView(
physics: const NeverScrollableScrollPhysics(),
controller: controller.pageController,
children: controller.pageList,
),
bottomNavigationBar: Obx(
() => BottomNavigationBar(
items: [
BottomNavigationBarItem(
icon: const Icon(Icons.translate),
label: 'interpretation'.tr,
),
BottomNavigationBarItem(
icon: const Icon(Icons.groups),
label: 'meeting'.tr,
),
],
backgroundColor: isDarkMode ? Colors.black : const Color(0xFFF9FAFB),
currentIndex: controller.pageIndex.value,
unselectedItemColor: isDarkMode ? Colors.grey[500] : const Color(0xFF6B7280),
selectedItemColor: const Color(0xFF3B82F6),
unselectedFontSize: 10.sp,
selectedFontSize: 10.sp,
onTap: (int index) {
if (index == controller.pageIndex.value) return;
controller.pageIndex.value = index;
controller.pageController.jumpToPage(index);
},
type: BottomNavigationBarType.fixed,
),
),
);
}
}

10
lib/routes/app_pages.dart

@ -34,8 +34,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/usage/views/translation_stats_view.dart';
import '../modules/usage/bindings/translation_stats_binding.dart';
import '../modules/usage/views/usage_view.dart';
import '../modules/usage/bindings/usage_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';
@ -197,9 +197,9 @@ abstract class AppPages {
transitionDuration: Duration(milliseconds: 250),
),
GetPage(
name: Routes.translationStats,
page: () => const TranslationStatsView(),
binding: TranslationStatsBinding(),
name: Routes.usageStats,
page: () => const UsageView(),
binding: UsageBinding(),
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),

2
lib/routes/app_routes.dart

@ -31,7 +31,7 @@ abstract class Routes {
static const pictureTranslation = '/picture_translation'; // 图片翻译
static const textTranslation = '/text_translation'; // 文本翻译
static const translationHistory = '/translation/history'; // 翻译历史
static const translationStats = '/translation/stats'; // 翻译统计
static const usageStats = '/usage/stats'; // 用量统计
static const speechDemo = '/speech_demo'; // 语音演示
static const translationList = '/translation/List'; // 翻译历史
static const ftftranslation = '/ftftranslation'; // 面对面翻译

Loading…
Cancel
Save