From caa166da7a129d319832397ffaaf0dbe6b226364 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Mon, 24 Nov 2025 20:20:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(usage):=20=E9=87=8D=E6=9E=84=E7=BF=BB?= =?UTF-8?q?=E8=AF=91=E7=BB=9F=E8=AE=A1=E4=B8=BA=E7=94=A8=E9=87=8F=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E5=B9=B6=E6=B7=BB=E5=8A=A0=E4=BC=9A=E8=AE=AE=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将翻译统计页面重构为用量统计,支持切换翻译和会议统计 - 新增会议统计相关功能,包括数据获取、聚合和展示 - 更新路由和绑定配置以支持新功能 - 添加会议统计的数据模型和服务方法 --- lib/data/services/usage_stats_service.dart | 43 +++++ .../controllers/explore_controller.dart | 6 +- lib/modules/explore/views/explore_view.dart | 2 +- .../bindings/translation_stats_binding.dart | 13 -- lib/modules/usage/bindings/usage_binding.dart | 20 ++ .../controllers/meeting_stats_controller.dart | 152 ++++++++++++++++ .../usage/controllers/usage_controller.dart | 20 ++ .../usage/views/meeting_stats_view.dart | 171 ++++++++++++++++++ lib/modules/usage/views/usage_view.dart | 50 +++++ lib/routes/app_pages.dart | 10 +- lib/routes/app_routes.dart | 2 +- 11 files changed, 466 insertions(+), 23 deletions(-) delete mode 100644 lib/modules/usage/bindings/translation_stats_binding.dart create mode 100644 lib/modules/usage/bindings/usage_binding.dart create mode 100644 lib/modules/usage/controllers/meeting_stats_controller.dart create mode 100644 lib/modules/usage/controllers/usage_controller.dart create mode 100644 lib/modules/usage/views/meeting_stats_view.dart create mode 100644 lib/modules/usage/views/usage_view.dart diff --git a/lib/data/services/usage_stats_service.dart b/lib/data/services/usage_stats_service.dart index 9a9d0003f..fc99643cb 100644 --- a/lib/data/services/usage_stats_service.dart +++ b/lib/data/services/usage_stats_service.dart @@ -543,4 +543,47 @@ class UsageStatsService extends GetxService { return TranslationUsageStats.empty(getCurrentUserId()); } } + + Future> getMeetingHistoryStats({ + required DateTime startDate, + required DateTime endDate, + }) async { + try { + final List 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 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, + ); + } } diff --git a/lib/modules/explore/controllers/explore_controller.dart b/lib/modules/explore/controllers/explore_controller.dart index d5c3b811b..6376e3fa4 100644 --- a/lib/modules/explore/controllers/explore_controller.dart +++ b/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 diff --git a/lib/modules/explore/views/explore_view.dart b/lib/modules/explore/views/explore_view.dart index 2c9c076ee..c501ab04b 100644 --- a/lib/modules/explore/views/explore_view.dart +++ b/lib/modules/explore/views/explore_view.dart @@ -120,7 +120,7 @@ class ExploreView extends GetView { color: isDarkMode ? Colors.white : Colors.grey[700], ), padding: EdgeInsets.zero, - onPressed: controller.goToTranslationStats, + onPressed: controller.goToUsageStats, ), ), ], diff --git a/lib/modules/usage/bindings/translation_stats_binding.dart b/lib/modules/usage/bindings/translation_stats_binding.dart deleted file mode 100644 index da6c55d3a..000000000 --- a/lib/modules/usage/bindings/translation_stats_binding.dart +++ /dev/null @@ -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()) { - Get.put(UsageStatsService(), permanent: true); - } - Get.lazyPut(() => TranslationStatsController()); - } -} diff --git a/lib/modules/usage/bindings/usage_binding.dart b/lib/modules/usage/bindings/usage_binding.dart new file mode 100644 index 000000000..1d3145df9 --- /dev/null +++ b/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()) { + Get.put(UsageStatsService(), permanent: true); + } + if (!Get.isRegistered()) { + Get.lazyPut(() => TranslationStatsController()); + } + Get.lazyPut(() => MeetingStatsController()); + Get.lazyPut(() => UsageController()); + } +} + diff --git a/lib/modules/usage/controllers/meeting_stats_controller.dart b/lib/modules/usage/controllers/meeting_stats_controller.dart new file mode 100644 index 000000000..bbcb7f9f6 --- /dev/null +++ b/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(); + + final isLoading = false.obs; + final selectedTabIndex = 0.obs; + final selectedTimeRange = '今日'.obs; + + final Rx todayStats = + MeetingAsrHistoryStats( + userId: '', + allAsrCalls: 0, + allAudioSeconds: 0, + allAudioFileBytes: 0, + allTranscriptBytes: 0, + ).obs; + final Rx weekStats = + MeetingAsrHistoryStats( + userId: '', + allAsrCalls: 0, + allAudioSeconds: 0, + allAudioFileBytes: 0, + allTranscriptBytes: 0, + ).obs; + final Rx monthStats = + MeetingAsrHistoryStats( + userId: '', + allAsrCalls: 0, + allAudioSeconds: 0, + allAudioFileBytes: 0, + allTranscriptBytes: 0, + ).obs; + + final RxList historyDaily = [].obs; + + @override + void onInit() { + super.onInit(); + loadAllStats(); + } + + Future 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 refreshData() async { + await loadAllStats(); + } +} + diff --git a/lib/modules/usage/controllers/usage_controller.dart b/lib/modules/usage/controllers/usage_controller.dart new file mode 100644 index 000000000..bf2c5a4be --- /dev/null +++ b/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 pageList = const [ + TranslationStatsView(), + MeetingStatsView(), + ]; + + @override + void onInit() { + super.onInit(); + pageController = PageController(initialPage: 0); + } +} diff --git a/lib/modules/usage/views/meeting_stats_view.dart b/lib/modules/usage/views/meeting_stats_view.dart new file mode 100644 index 000000000..e648dc14f --- /dev/null +++ b/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 { + 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), + ), + ], + ); + } +} + diff --git a/lib/modules/usage/views/usage_view.dart b/lib/modules/usage/views/usage_view.dart new file mode 100644 index 000000000..efa1b02e8 --- /dev/null +++ b/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 { + 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, + ), + ), + ); + } +} + diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart index 4f9b22eb5..a45b799f9 100644 --- a/lib/routes/app_pages.dart +++ b/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), ), diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index c964ffb57..f76c442cb 100644 --- a/lib/routes/app_routes.dart +++ b/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'; // 面对面翻译