Browse Source

feat(会议统计): 添加对象存储统计功能并重构时间范围选择

添加会议对象存储历史统计功能,包括数据获取和聚合方法
重构时间范围选择逻辑,移除周/月统计,增加自定义日期范围选择
更新界面显示,添加对象存储统计卡片和日期范围选择器
newdev_shunjiawei
lxm 10 months ago
parent
commit
c9c3b939d2
  1. 61
      lib/data/services/usage_stats_service.dart
  2. 132
      lib/modules/usage/controllers/meeting_stats_controller.dart
  3. 145
      lib/modules/usage/views/meeting_stats_view.dart

61
lib/data/services/usage_stats_service.dart

@ -673,6 +673,9 @@ class UsageStatsService extends GetxService {
} }
} }
/// 获取会议历史统计
///
/// 从[startDate]到[endDate]的会议ASR记录。
Future<List<MeetingAsrDailyStats>> getMeetingHistoryStats({ Future<List<MeetingAsrDailyStats>> getMeetingHistoryStats({
required DateTime startDate, required DateTime startDate,
required DateTime endDate, required DateTime endDate,
@ -697,6 +700,9 @@ class UsageStatsService extends GetxService {
} }
} }
/// 聚合会议统计
///
/// 从[daily]中聚合会议统计数据。
MeetingAsrHistoryStats aggregateMeetingStats( MeetingAsrHistoryStats aggregateMeetingStats(
List<MeetingAsrDailyStats> daily) { List<MeetingAsrDailyStats> daily) {
int calls = 0; int calls = 0;
@ -719,4 +725,59 @@ class UsageStatsService extends GetxService {
allTranscriptBytes: transcriptBytes, allTranscriptBytes: transcriptBytes,
); );
} }
/// 获取会议对象存储历史统计
///
/// 从[startDate]到[endDate]的会议对象存储上传记录。
Future<List<MeetingStorageDailyStats>> getMeetingStorageHistoryStats({
required DateTime startDate,
required DateTime endDate,
}) async {
try {
final List<MeetingStorageDailyStats> 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<MeetingObjectStorageRecord> uploads = [];
if (raw is List) {
uploads = raw
.map((e) => MeetingObjectStorageRecord.fromJson(
Map<String, dynamic>.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<MeetingStorageDailyStats> 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,
);
}
} }

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

@ -17,23 +17,49 @@ class MeetingStatsController extends GetxController {
allAudioFileBytes: 0, allAudioFileBytes: 0,
allTranscriptBytes: 0, allTranscriptBytes: 0,
).obs; ).obs;
final Rx<MeetingAsrHistoryStats> weekStats = MeetingAsrHistoryStats( final Rx<MeetingAsrHistoryStats> historyStats = MeetingAsrHistoryStats(
userId: '', userId: '',
allAsrCalls: 0, allAsrCalls: 0,
allAudioSeconds: 0, allAudioSeconds: 0,
allAudioFileBytes: 0, allAudioFileBytes: 0,
allTranscriptBytes: 0, allTranscriptBytes: 0,
).obs; ).obs;
final Rx<MeetingAsrHistoryStats> monthStats = MeetingAsrHistoryStats(
final RxList<MeetingAsrDailyStats> historyDaily =
<MeetingAsrDailyStats>[].obs;
final Rx<MeetingStorageHistoryStats> todayStorageStats =
MeetingStorageHistoryStats(
userId: '',
allStorageUploads: 0,
allStorageBytes: 0,
allTransferBytes: 0,
).obs;
final Rx<MeetingStorageHistoryStats> historyStorageStats =
MeetingStorageHistoryStats(
userId: '',
allStorageUploads: 0,
allStorageBytes: 0,
allTransferBytes: 0,
).obs;
final Rx<DateTime?> rangeStart = Rx<DateTime?>(null);
final Rx<DateTime?> rangeEnd = Rx<DateTime?>(null);
final Rx<MeetingAsrHistoryStats> rangeStats = MeetingAsrHistoryStats(
userId: '', userId: '',
allAsrCalls: 0, allAsrCalls: 0,
allAudioSeconds: 0, allAudioSeconds: 0,
allAudioFileBytes: 0, allAudioFileBytes: 0,
allTranscriptBytes: 0, allTranscriptBytes: 0,
).obs; ).obs;
final Rx<MeetingStorageHistoryStats> rangeStorageStats =
final RxList<MeetingAsrDailyStats> historyDaily = MeetingStorageHistoryStats(
<MeetingAsrDailyStats>[].obs; userId: '',
allStorageUploads: 0,
allStorageBytes: 0,
allTransferBytes: 0,
).obs;
@override @override
void onInit() { void onInit() {
@ -52,28 +78,53 @@ class MeetingStatsController extends GetxController {
); );
todayStats.value = _usageService.aggregateMeetingStats(todayList); todayStats.value = _usageService.aggregateMeetingStats(todayList);
final weekStart = now.subtract(Duration(days: now.weekday - 1)); final historyStart = now.subtract(const Duration(days: 7));
final weekEnd = weekStart.add(const Duration(days: 6)); historyDaily.value = await _usageService.getMeetingHistoryStats(
final weekList = await _usageService.getMeetingHistoryStats( startDate:
startDate: DateTime(weekStart.year, weekStart.month, weekStart.day), DateTime(historyStart.year, historyStart.month, historyStart.day),
endDate: DateTime(weekEnd.year, weekEnd.month, weekEnd.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 todayStorageDaily =
final monthEnd = DateTime(now.year, now.month + 1, 0); await _usageService.getMeetingStorageHistoryStats(
final monthList = await _usageService.getMeetingHistoryStats( startDate: DateTime(now.year, now.month, now.day),
startDate: monthStart, endDate: DateTime(now.year, now.month, now.day),
endDate: monthEnd,
); );
monthStats.value = _usageService.aggregateMeetingStats(monthList); todayStorageStats.value =
_usageService.aggregateMeetingStorageStats(todayStorageDaily);
final historyStart = now.subtract(const Duration(days: 7)); final historyStorageDaily =
historyDaily.value = await _usageService.getMeetingHistoryStats( await _usageService.getMeetingStorageHistoryStats(
startDate: startDate:
DateTime(historyStart.year, historyStart.month, historyStart.day), DateTime(historyStart.year, historyStart.month, historyStart.day),
endDate: DateTime(now.year, now.month, now.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('会议统计数据加载完成'); Logger.info('会议统计数据加载完成');
} catch (e) { } catch (e) {
@ -90,14 +141,11 @@ class MeetingStatsController extends GetxController {
selectedTimeRange.value = 'today'.tr; // 今日 selectedTimeRange.value = 'today'.tr; // 今日
break; break;
case 1: case 1:
selectedTimeRange.value = 'thisWeek'.tr; // 本周
break;
case 2:
selectedTimeRange.value = 'thisMonth'.tr; // 本月
break;
case 3:
selectedTimeRange.value = 'history'.tr; // 历史 selectedTimeRange.value = 'history'.tr; // 历史
break; break;
default:
selectedTimeRange.value = 'today'.tr; // 今日
break;
} }
} }
@ -106,9 +154,7 @@ class MeetingStatsController extends GetxController {
case 0: case 0:
return todayStats.value; return todayStats.value;
case 1: case 1:
return weekStats.value; return rangeStats.value;
case 2:
return monthStats.value;
default: default:
return todayStats.value; return todayStats.value;
} }
@ -147,4 +193,34 @@ class MeetingStatsController extends GetxController {
Future<void> refreshData() async { Future<void> refreshData() async {
await loadAllStats(); await loadAllStats();
} }
Future<void> 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';
}
} }

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

@ -4,6 +4,7 @@ import 'package:get/get.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
import '../controllers/meeting_stats_controller.dart'; import '../controllers/meeting_stats_controller.dart';
import '../../../data/models/user_Info.dart'; import '../../../data/models/user_Info.dart';
import '../../../data/models/meeting_usage_stats.dart';
class MeetingStatsView extends GetView<MeetingStatsController> { class MeetingStatsView extends GetView<MeetingStatsController> {
const MeetingStatsView({Key? key}) : super(key: key); const MeetingStatsView({Key? key}) : super(key: key);
@ -14,14 +15,14 @@ class MeetingStatsView extends GetView<MeetingStatsController> {
return Scaffold( return Scaffold(
backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.grey[50], backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.grey[50],
appBar: _buildAppBar(isDarkMode), appBar: _buildAppBar(context, isDarkMode),
body: Obx(() => controller.isLoading.value body: Obx(() => controller.isLoading.value
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: _buildBody(isDarkMode)), : _buildBody(isDarkMode)),
); );
} }
PreferredSizeWidget _buildAppBar(bool isDarkMode) { PreferredSizeWidget _buildAppBar(BuildContext context, bool isDarkMode) {
return AppBar( return AppBar(
leading: IconButton( leading: IconButton(
icon: Icon(Icons.arrow_back_ios, icon: Icon(Icons.arrow_back_ios,
@ -39,6 +40,27 @@ class MeetingStatsView extends GetView<MeetingStatsController> {
), ),
centerTitle: true, centerTitle: true,
actions: [ 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( IconButton(
icon: Icon(Icons.refresh, icon: Icon(Icons.refresh,
color: isDarkMode ? Colors.white : Colors.black), color: isDarkMode ? Colors.white : Colors.black),
@ -49,16 +71,32 @@ class MeetingStatsView extends GetView<MeetingStatsController> {
} }
Widget _buildBody(bool isDarkMode) { 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( return Column(
children: [ children: [
_buildIntegralRow(isDarkMode), _buildIntegralRow(isDarkMode),
_buildTimeRangeSelector(isDarkMode),
Expanded( Expanded(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: EdgeInsets.all(16.w), padding: EdgeInsets.all(16.w),
child: Column( child: Column(
children: [ 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<MeetingStatsController> {
}); });
} }
Widget _buildTimeRangeSelector(bool isDarkMode) { Widget _buildOverviewCard(
return Container( String title,
margin: EdgeInsets.all(16.w), bool isDarkMode,
decoration: BoxDecoration( MeetingAsrHistoryStats asrStats,
color: isDarkMode ? AppColors.cardBackground : Colors.white, MeetingStorageHistoryStats storageStats,
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;
return Container( return Container(
padding: EdgeInsets.all(16.w), padding: EdgeInsets.all(16.w),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -185,13 +175,13 @@ class MeetingStatsView extends GetView<MeetingStatsController> {
children: [ children: [
Icon(Icons.dashboard, color: AppColors.primary, size: 20.sp), Icon(Icons.dashboard, color: AppColors.primary, size: 20.sp),
SizedBox(width: 8.w), SizedBox(width: 8.w),
Obx(() => Text( Text(
'${controller.selectedTimeRange.value}${'overview'.tr}', '$title${'overview'.tr}',
style: TextStyle( style: TextStyle(
fontSize: 16.sp, fontSize: 16.sp,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black), color: isDarkMode ? Colors.white : Colors.black),
)), ),
], ],
), ),
SizedBox(height: 16.h), SizedBox(height: 16.h),
@ -200,13 +190,13 @@ class MeetingStatsView extends GetView<MeetingStatsController> {
Expanded( Expanded(
child: _buildStatItem( child: _buildStatItem(
'asrApiCalls'.tr, // ASR调用次数 'asrApiCalls'.tr, // ASR调用次数
controller.formatNumber(stats.allAsrCalls), controller.formatNumber(asrStats.allAsrCalls),
Icons.api, Icons.api,
isDarkMode)), isDarkMode)),
Expanded( Expanded(
child: _buildStatItem( child: _buildStatItem(
'totalAudioDuration'.tr, // 音频总时长 'totalAudioDuration'.tr, // 音频总时长
controller.formatDuration(stats.allAudioSeconds), controller.formatDuration(asrStats.allAudioSeconds),
Icons.mic, Icons.mic,
isDarkMode)), isDarkMode)),
], ],
@ -217,17 +207,34 @@ class MeetingStatsView extends GetView<MeetingStatsController> {
Expanded( Expanded(
child: _buildStatItem( child: _buildStatItem(
'audioFileSize'.tr, // 音频文件大小 'audioFileSize'.tr, // 音频文件大小
controller.formatBytes(stats.allAudioFileBytes), controller.formatBytes(asrStats.allAudioFileBytes),
Icons.folder, Icons.folder,
isDarkMode)), isDarkMode)),
Expanded( Expanded(
child: _buildStatItem( child: _buildStatItem(
'transcriptBytes'.tr, // 转写字节 'transcriptBytes'.tr, // 转写字节
controller.formatNumber(stats.allTranscriptBytes), controller.formatNumber(asrStats.allTranscriptBytes),
Icons.text_snippet, Icons.text_snippet,
isDarkMode)), 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)),
],
),
], ],
), ),
); );

Loading…
Cancel
Save