Browse Source
- 将翻译统计页面重构为用量统计,支持切换翻译和会议统计 - 新增会议统计相关功能,包括数据获取、聚合和展示 - 更新路由和绑定配置以支持新功能 - 添加会议统计的数据模型和服务方法newdev_shunjiawei
11 changed files with 466 additions and 23 deletions
@ -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()); |
|
||||
} |
|
||||
} |
|
||||
@ -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()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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), |
||||
|
), |
||||
|
], |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -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, |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
Loading…
Reference in new issue