From a32cb8a74106041fe673ed0928e2382310df066a Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Mon, 15 Dec 2025 18:02:29 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E4=BC=9A=E8=AE=AE=E7=BB=9F=E8=AE=A1):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=AF=B9=E8=B1=A1=E5=AD=98=E5=82=A8=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E5=8A=9F=E8=83=BD=E5=B9=B6=E9=87=8D=E6=9E=84=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E8=8C=83=E5=9B=B4=E9=80=89=E6=8B=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加会议对象存储历史统计功能,包括数据获取和聚合方法 重构时间范围选择逻辑,移除周/月统计,增加自定义日期范围选择 更新界面显示,添加对象存储统计卡片和日期范围选择器 --- lib/data/services/usage_stats_service.dart | 61 ++++++++ .../controllers/meeting_stats_controller.dart | 132 ++++++++++++---- .../usage/views/meeting_stats_view.dart | 145 +++++++++--------- 3 files changed, 241 insertions(+), 97 deletions(-) diff --git a/lib/data/services/usage_stats_service.dart b/lib/data/services/usage_stats_service.dart index e0f76a799..969e2ea55 100644 --- a/lib/data/services/usage_stats_service.dart +++ b/lib/data/services/usage_stats_service.dart @@ -673,6 +673,9 @@ class UsageStatsService extends GetxService { } } + /// 获取会议历史统计 + /// + /// 从[startDate]到[endDate]的会议ASR记录。 Future> getMeetingHistoryStats({ required DateTime startDate, required DateTime endDate, @@ -697,6 +700,9 @@ class UsageStatsService extends GetxService { } } + /// 聚合会议统计 + /// + /// 从[daily]中聚合会议统计数据。 MeetingAsrHistoryStats aggregateMeetingStats( List daily) { int calls = 0; @@ -719,4 +725,59 @@ class UsageStatsService extends GetxService { allTranscriptBytes: transcriptBytes, ); } + + /// 获取会议对象存储历史统计 + /// + /// 从[startDate]到[endDate]的会议对象存储上传记录。 + Future> getMeetingStorageHistoryStats({ + 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 raw = _storage.read('${_meetingOsDailykey}_$key'); + List uploads = []; + if (raw is List) { + uploads = raw + .map((e) => MeetingObjectStorageRecord.fromJson( + Map.from(e))) + .toList(); + } + if (uploads.isNotEmpty) { + list.add(MeetingStorageDailyStats( + date: key, userId: getCurrentUserId(), items: uploads)); + } + } + Logger.i('MeetingOSrecord', + '获取会议对象存储历史: start=${_formatDate(startDate)}, end=${_formatDate(endDate)}, 有效天数=${list.length}'); + return list; + } catch (e) { + Logger.error('获取对象存储历史统计失败: $e'); + return []; + } + } + + MeetingStorageHistoryStats aggregateMeetingStorageStats( + List daily) { + int uploads = 0; + int storageBytes = 0; + int transferBytes = 0; + for (final d in daily) { + uploads += d.storageUploads; + storageBytes += d.storageBytesTotal; + transferBytes += d.transferBytesTotal; + } + Logger.i('MeetingOSrecord', + '聚合对象存储统计: uploads=${uploads}, storageBytes=${storageBytes}, transferBytes=${transferBytes}'); + return MeetingStorageHistoryStats( + userId: getCurrentUserId(), + allStorageUploads: uploads, + allStorageBytes: storageBytes, + allTransferBytes: transferBytes, + ); + } } diff --git a/lib/modules/usage/controllers/meeting_stats_controller.dart b/lib/modules/usage/controllers/meeting_stats_controller.dart index 147c709c7..2aad0d20a 100644 --- a/lib/modules/usage/controllers/meeting_stats_controller.dart +++ b/lib/modules/usage/controllers/meeting_stats_controller.dart @@ -17,23 +17,49 @@ class MeetingStatsController extends GetxController { allAudioFileBytes: 0, allTranscriptBytes: 0, ).obs; - final Rx weekStats = MeetingAsrHistoryStats( + final Rx historyStats = MeetingAsrHistoryStats( userId: '', allAsrCalls: 0, allAudioSeconds: 0, allAudioFileBytes: 0, allTranscriptBytes: 0, ).obs; - final Rx monthStats = MeetingAsrHistoryStats( + + final RxList historyDaily = + [].obs; + + final Rx todayStorageStats = + MeetingStorageHistoryStats( + userId: '', + allStorageUploads: 0, + allStorageBytes: 0, + allTransferBytes: 0, + ).obs; + + final Rx historyStorageStats = + MeetingStorageHistoryStats( + userId: '', + allStorageUploads: 0, + allStorageBytes: 0, + allTransferBytes: 0, + ).obs; + + final Rx rangeStart = Rx(null); + final Rx rangeEnd = Rx(null); + final Rx rangeStats = MeetingAsrHistoryStats( userId: '', allAsrCalls: 0, allAudioSeconds: 0, allAudioFileBytes: 0, allTranscriptBytes: 0, ).obs; - - final RxList historyDaily = - [].obs; + final Rx rangeStorageStats = + MeetingStorageHistoryStats( + userId: '', + allStorageUploads: 0, + allStorageBytes: 0, + allTransferBytes: 0, + ).obs; @override void onInit() { @@ -52,28 +78,53 @@ class MeetingStatsController extends GetxController { ); 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), + 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), ); - weekStats.value = _usageService.aggregateMeetingStats(weekList); + historyStats.value = + _usageService.aggregateMeetingStats(historyDaily.value); - 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, + final todayStorageDaily = + await _usageService.getMeetingStorageHistoryStats( + startDate: DateTime(now.year, now.month, now.day), + endDate: DateTime(now.year, now.month, now.day), ); - monthStats.value = _usageService.aggregateMeetingStats(monthList); + todayStorageStats.value = + _usageService.aggregateMeetingStorageStats(todayStorageDaily); - final historyStart = now.subtract(const Duration(days: 7)); - historyDaily.value = await _usageService.getMeetingHistoryStats( + final historyStorageDaily = + await _usageService.getMeetingStorageHistoryStats( startDate: DateTime(historyStart.year, historyStart.month, historyStart.day), endDate: DateTime(now.year, now.month, now.day), ); + historyStorageStats.value = + _usageService.aggregateMeetingStorageStats(historyStorageDaily); + + if (rangeStart.value != null && rangeEnd.value != null) { + final customAsrDaily = await _usageService.getMeetingHistoryStats( + startDate: DateTime(rangeStart.value!.year, rangeStart.value!.month, + rangeStart.value!.day), + endDate: DateTime( + rangeEnd.value!.year, rangeEnd.value!.month, rangeEnd.value!.day), + ); + rangeStats.value = _usageService.aggregateMeetingStats(customAsrDaily); + final customStorageDaily = + await _usageService.getMeetingStorageHistoryStats( + startDate: DateTime(rangeStart.value!.year, rangeStart.value!.month, + rangeStart.value!.day), + endDate: DateTime( + rangeEnd.value!.year, rangeEnd.value!.month, rangeEnd.value!.day), + ); + rangeStorageStats.value = + _usageService.aggregateMeetingStorageStats(customStorageDaily); + } else { + rangeStats.value = historyStats.value; + rangeStorageStats.value = historyStorageStats.value; + } Logger.info('会议统计数据加载完成'); } catch (e) { @@ -90,14 +141,11 @@ class MeetingStatsController extends GetxController { selectedTimeRange.value = 'today'.tr; // 今日 break; case 1: - selectedTimeRange.value = 'thisWeek'.tr; // 本周 - break; - case 2: - selectedTimeRange.value = 'thisMonth'.tr; // 本月 - break; - case 3: selectedTimeRange.value = 'history'.tr; // 历史 break; + default: + selectedTimeRange.value = 'today'.tr; // 今日 + break; } } @@ -106,9 +154,7 @@ class MeetingStatsController extends GetxController { case 0: return todayStats.value; case 1: - return weekStats.value; - case 2: - return monthStats.value; + return rangeStats.value; default: return todayStats.value; } @@ -147,4 +193,34 @@ class MeetingStatsController extends GetxController { Future refreshData() async { await loadAllStats(); } + + Future setCustomRangeDates(DateTime start, DateTime end) async { + rangeStart.value = start; + rangeEnd.value = end; + isLoading.value = true; + try { + final asrDaily = await _usageService.getMeetingHistoryStats( + startDate: DateTime(start.year, start.month, start.day), + endDate: DateTime(end.year, end.month, end.day), + ); + rangeStats.value = _usageService.aggregateMeetingStats(asrDaily); + final storageDaily = await _usageService.getMeetingStorageHistoryStats( + startDate: DateTime(start.year, start.month, start.day), + endDate: DateTime(end.year, end.month, end.day), + ); + rangeStorageStats.value = + _usageService.aggregateMeetingStorageStats(storageDaily); + } catch (e) { + Logger.error('设置自定义日期范围失败: $e'); + } finally { + isLoading.value = false; + } + } + + String formatDate(DateTime d) { + final y = d.year.toString(); + final m = d.month.toString().padLeft(2, '0'); + final day = d.day.toString().padLeft(2, '0'); + return '$y-$m-$day'; + } } diff --git a/lib/modules/usage/views/meeting_stats_view.dart b/lib/modules/usage/views/meeting_stats_view.dart index 140a40447..0702a4aa1 100644 --- a/lib/modules/usage/views/meeting_stats_view.dart +++ b/lib/modules/usage/views/meeting_stats_view.dart @@ -4,6 +4,7 @@ import 'package:get/get.dart'; import '../../../core/theme/app_colors.dart'; import '../controllers/meeting_stats_controller.dart'; import '../../../data/models/user_Info.dart'; +import '../../../data/models/meeting_usage_stats.dart'; class MeetingStatsView extends GetView { const MeetingStatsView({Key? key}) : super(key: key); @@ -14,14 +15,14 @@ class MeetingStatsView extends GetView { return Scaffold( backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.grey[50], - appBar: _buildAppBar(isDarkMode), + appBar: _buildAppBar(context, isDarkMode), body: Obx(() => controller.isLoading.value ? const Center(child: CircularProgressIndicator()) : _buildBody(isDarkMode)), ); } - PreferredSizeWidget _buildAppBar(bool isDarkMode) { + PreferredSizeWidget _buildAppBar(BuildContext context, bool isDarkMode) { return AppBar( leading: IconButton( icon: Icon(Icons.arrow_back_ios, @@ -39,6 +40,27 @@ class MeetingStatsView extends GetView { ), centerTitle: true, actions: [ + IconButton( + icon: Icon(Icons.date_range, + color: isDarkMode ? Colors.white : Colors.black), + onPressed: () async { + final now = DateTime.now(); + final initialStart = controller.rangeStart.value ?? + DateTime(now.year, now.month, now.day); + final initialEnd = controller.rangeEnd.value ?? + DateTime(now.year, now.month, now.day); + final picked = await showDateRangePicker( + context: context, + firstDate: DateTime(now.year - 1), + lastDate: DateTime(now.year + 1), + initialDateRange: + DateTimeRange(start: initialStart, end: initialEnd), + ); + if (picked != null) { + await controller.setCustomRangeDates(picked.start, picked.end); + } + }, + ), IconButton( icon: Icon(Icons.refresh, color: isDarkMode ? Colors.white : Colors.black), @@ -49,16 +71,32 @@ class MeetingStatsView extends GetView { } Widget _buildBody(bool isDarkMode) { + final hasRange = controller.rangeStart.value != null && + controller.rangeEnd.value != null; + final historyTitle = hasRange + ? "${'history'.tr} (${controller.formatDate(controller.rangeStart.value!)} ~ ${controller.formatDate(controller.rangeEnd.value!)})" + : 'history'.tr; return Column( children: [ _buildIntegralRow(isDarkMode), - _buildTimeRangeSelector(isDarkMode), Expanded( child: SingleChildScrollView( padding: EdgeInsets.all(16.w), child: Column( children: [ - _buildOverviewCard(isDarkMode), + _buildOverviewCard( + 'today'.tr, // 今日 + isDarkMode, + controller.todayStats.value, + controller.todayStorageStats.value, + ), + SizedBox(height: 16.h), + _buildOverviewCard( + historyTitle, + isDarkMode, + controller.rangeStats.value, + controller.rangeStorageStats.value, + ), ], ), ), @@ -112,60 +150,12 @@ class MeetingStatsView extends GetView { }); } - 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('today'.tr, 0, isDarkMode), // 今日 - _buildTimeRangeTab('thisWeek'.tr, 1, isDarkMode), // 本周 - _buildTimeRangeTab('thisMonth'.tr, 2, isDarkMode), // 本月 - _buildTimeRangeTab('history'.tr, 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; + Widget _buildOverviewCard( + String title, + bool isDarkMode, + MeetingAsrHistoryStats asrStats, + MeetingStorageHistoryStats storageStats, + ) { return Container( padding: EdgeInsets.all(16.w), decoration: BoxDecoration( @@ -185,13 +175,13 @@ class MeetingStatsView extends GetView { children: [ Icon(Icons.dashboard, color: AppColors.primary, size: 20.sp), SizedBox(width: 8.w), - Obx(() => Text( - '${controller.selectedTimeRange.value}${'overview'.tr}', - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black), - )), + Text( + '$title${'overview'.tr}', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black), + ), ], ), SizedBox(height: 16.h), @@ -200,13 +190,13 @@ class MeetingStatsView extends GetView { Expanded( child: _buildStatItem( 'asrApiCalls'.tr, // ASR调用次数 - controller.formatNumber(stats.allAsrCalls), + controller.formatNumber(asrStats.allAsrCalls), Icons.api, isDarkMode)), Expanded( child: _buildStatItem( 'totalAudioDuration'.tr, // 音频总时长 - controller.formatDuration(stats.allAudioSeconds), + controller.formatDuration(asrStats.allAudioSeconds), Icons.mic, isDarkMode)), ], @@ -217,17 +207,34 @@ class MeetingStatsView extends GetView { Expanded( child: _buildStatItem( 'audioFileSize'.tr, // 音频文件大小 - controller.formatBytes(stats.allAudioFileBytes), + controller.formatBytes(asrStats.allAudioFileBytes), Icons.folder, isDarkMode)), Expanded( child: _buildStatItem( 'transcriptBytes'.tr, // 转写字节 - controller.formatNumber(stats.allTranscriptBytes), + controller.formatNumber(asrStats.allTranscriptBytes), Icons.text_snippet, isDarkMode)), ], ), + SizedBox(height: 12.h), + Row( + children: [ + Expanded( + child: _buildStatItem( + 'storageUploads'.tr, // 对象存储上传次数 + controller.formatNumber(storageStats.allStorageUploads), + Icons.cloud_upload, + isDarkMode)), + Expanded( + child: _buildStatItem( + 'storageBytes'.tr, // 对象存储总大小 + controller.formatBytes(storageStats.allStorageBytes), + Icons.cloud, + isDarkMode)), + ], + ), ], ), );