Browse Source
- 将翻译统计页面重构为用量统计,支持切换翻译和会议统计 - 新增会议统计相关功能,包括数据获取、聚合和展示 - 更新路由和绑定配置以支持新功能 - 添加会议统计的数据模型和服务方法weicu
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