Browse Source

feat(usage): 重构翻译统计为用量统计并添加会议统计功能

- 将翻译统计页面重构为用量统计,支持切换翻译和会议统计
- 新增会议统计相关功能,包括数据获取、聚合和展示
- 更新路由和绑定配置以支持新功能
- 添加会议统计的数据模型和服务方法
weicu
lxm 11 months ago
parent
commit
caa166da7a
  1. 43
      lib/data/services/usage_stats_service.dart
  2. 6
      lib/modules/explore/controllers/explore_controller.dart
  3. 2
      lib/modules/explore/views/explore_view.dart
  4. 13
      lib/modules/usage/bindings/translation_stats_binding.dart
  5. 20
      lib/modules/usage/bindings/usage_binding.dart
  6. 152
      lib/modules/usage/controllers/meeting_stats_controller.dart
  7. 20
      lib/modules/usage/controllers/usage_controller.dart
  8. 171
      lib/modules/usage/views/meeting_stats_view.dart
  9. 50
      lib/modules/usage/views/usage_view.dart
  10. 10
      lib/routes/app_pages.dart
  11. 2
      lib/routes/app_routes.dart

43
lib/data/services/usage_stats_service.dart

@ -543,4 +543,47 @@ class UsageStatsService extends GetxService {
return TranslationUsageStats.empty(getCurrentUserId()); 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() { void goToUsageStats() {
Get.toNamed(Routes.translationStats); Get.toNamed(Routes.usageStats);
} }
@override @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], color: isDarkMode ? Colors.white : Colors.grey[700],
), ),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
onPressed: controller.goToTranslationStats, onPressed: controller.goToUsageStats,
), ),
), ),
], ],

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();
}
}

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),
),
],
);
}
}

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

2
lib/routes/app_routes.dart

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

Loading…
Cancel
Save