Browse Source

Merge branch 'new_dev' into new_ios

newdev_shunjiawei
fdp 1 year ago
parent
commit
269472539d
  1. 1
      .gitignore
  2. 1
      .vercel/project.json
  3. BIN
      assets/fonts/NotoSansSC-Regular.ttf
  4. BIN
      assets/images/headphone_new3.png
  5. BIN
      assets/images/headphone_new4.png
  6. BIN
      assets/images/mine-background.png
  7. 201
      ios/Podfile.lock
  8. 2
      lib/core/bindings/initial_binding.dart
  9. 56
      lib/core/translations/language/en_us.dart
  10. 38
      lib/core/translations/language/zh_cn.dart
  11. 27
      lib/core/utils/permission_util.dart
  12. 12
      lib/data/models/appconfig_model.dart
  13. 7
      lib/data/models/appconfig_model.g.dart
  14. 2
      lib/data/models/channel_app_model.dart
  15. 2
      lib/data/models/channel_app_model.g.dart
  16. 450
      lib/data/models/translation_usage_stats.dart
  17. 9
      lib/data/models/user_Info.dart
  18. 19
      lib/data/services/db/sqflite_api.dart
  19. 10
      lib/data/services/location_manager.dart
  20. 10
      lib/data/services/meeting/meeting_task_service.dart
  21. 12
      lib/data/services/music_manager.dart
  22. 519
      lib/data/services/translation_usage_service.dart
  23. 2
      lib/data/services/user_portrait.dart
  24. 38
      lib/data/services/version_update_service.dart
  25. 65
      lib/modules/agent/controllers/agent_controller.dart
  26. 357
      lib/modules/agent/views/agent_view.dart
  27. 77
      lib/modules/agent/views/message_bubble.dart
  28. 324
      lib/modules/agent/views/navigation_card.dart
  29. 2
      lib/modules/common/widgets/version_update_dialog.dart
  30. 60
      lib/modules/explore/controllers/explore_controller.dart
  31. 365
      lib/modules/explore/views/explore_view.dart
  32. 117
      lib/modules/home/controllers/home_controller.dart
  33. 56
      lib/modules/home/views/home_view.dart
  34. 71
      lib/modules/login/controllers/login_controller.dart
  35. 14
      lib/modules/login/views/login_view.dart
  36. 6
      lib/modules/meeting/bindings/meeting_binding.dart
  37. 4
      lib/modules/meeting/controllers/meeting_connect_controller.dart
  38. 398
      lib/modules/meeting/controllers/meeting_controller.dart
  39. 37
      lib/modules/meeting/controllers/meeting_details_controller.dart
  40. 420
      lib/modules/meeting/controllers/meeting_home_controller.dart
  41. 121
      lib/modules/meeting/controllers/meeting_mine_controller.dart
  42. 10
      lib/modules/meeting/controllers/meeting_record_controller.dart
  43. 21
      lib/modules/meeting/controllers/mind_map_controller.dart
  44. 3
      lib/modules/meeting/model/meeting_model.dart
  45. 63
      lib/modules/meeting/utils/meeting_ui_utils.dart
  46. 5
      lib/modules/meeting/views/bottomSheet/ai_bottom_sheet.dart
  47. 10
      lib/modules/meeting/views/bottomSheet/edit_title_bottom_sheet.dart
  48. 284
      lib/modules/meeting/views/bottomSheet/filtrate_drawer.dart
  49. 9
      lib/modules/meeting/views/bottomSheet/generate_bottom_sheet.dart
  50. 30
      lib/modules/meeting/views/bottomSheet/import_audio_bottom_sheet.dart
  51. 17
      lib/modules/meeting/views/bottomSheet/more_bottom_sheet.dart
  52. 114
      lib/modules/meeting/views/bottomSheet/navigation_bar_bottom_sheet.dart
  53. 164
      lib/modules/meeting/views/bottomSheet/second_level_bottom_sheet.dart
  54. 90
      lib/modules/meeting/views/bottomSheet/share_bottom_sheet.dart
  55. 2
      lib/modules/meeting/views/bottomSheet/speaker_all_bottom_sheet.dart
  56. 1
      lib/modules/meeting/views/bottomSheet/speaker_bottom_sheet.dart
  57. 383
      lib/modules/meeting/views/bottomSheet/statistics_bottom_sheet.dart
  58. 60
      lib/modules/meeting/views/meeting_connect_view.dart
  59. 724
      lib/modules/meeting/views/meeting_details_view.dart
  60. 474
      lib/modules/meeting/views/meeting_home_view.dart
  61. 439
      lib/modules/meeting/views/meeting_mine_view.dart
  62. 506
      lib/modules/meeting/views/meeting_view.dart
  63. 39
      lib/modules/meeting/views/tabs/overview_tab.dart
  64. 22
      lib/modules/meeting/views/tabs/speech_tab.dart
  65. 178
      lib/modules/meeting/views/tabs/summary_tab.dart
  66. 2
      lib/modules/meeting/views/template/template_all_view.dart
  67. 0
      lib/modules/meeting/views/template/template_view.dart
  68. 273
      lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart
  69. 96
      lib/modules/pairing/controllers/pairing_controller.dart
  70. 342
      lib/modules/pairing/views/pairing_view.dart
  71. 7
      lib/modules/pairing/views/pairing_view_new.dart
  72. 64
      lib/modules/profile/controllers/profile_controller.dart
  73. 488
      lib/modules/profile/views/profile_meeting_view.dart
  74. 54
      lib/modules/profile/views/profile_view.dart
  75. 137
      lib/modules/profile/views/unified_avatar.dart
  76. 8
      lib/modules/settings/bindings/settings_binding.dart
  77. 165
      lib/modules/settings/controllers/settings_controller.dart
  78. 201
      lib/modules/settings/views/settings_view.dart
  79. 36
      lib/modules/splash/controllers/splash_controller.dart
  80. 18
      lib/modules/translation/bindings/translation_stats_binding.dart
  81. 178
      lib/modules/translation/controllers/translation_controller.dart
  82. 258
      lib/modules/translation/controllers/translation_cost_controller.dart
  83. 25
      lib/modules/translation/controllers/translation_detail_controller.dart
  84. 358
      lib/modules/translation/controllers/translation_stats_controller.dart
  85. 2
      lib/modules/translation/views/translation_detail_view.dart
  86. 757
      lib/modules/translation/views/translation_stats_view.dart
  87. 158
      lib/modules/translation/views/translation_view.dart
  88. 17
      lib/routes/app_pages.dart
  89. 4
      lib/routes/app_routes.dart
  90. 64
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  91. 16
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt
  92. 56
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  93. 10
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift
  94. 15
      local_plugins/agent_service/lib/agent_service.dart
  95. 4
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  96. 83
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  97. 4
      local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleConst.swift
  98. 9
      local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift
  99. 17
      pubspec.yaml

1
.gitignore

@ -221,3 +221,4 @@ fastlane/test_output
fastlane/readme.md
.trae/rules/project_rules.md
.augment/rules/imported/project_rules.md
.kiro/

1
.vercel/project.json

@ -0,0 +1 @@
{"projectName":"trae_deep_voice_k3cu"}

BIN
assets/fonts/NotoSansSC-Regular.ttf

Binary file not shown.

BIN
assets/images/headphone_new3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 774 KiB

BIN
assets/images/headphone_new4.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 904 KiB

BIN
assets/images/mine-background.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

201
ios/Podfile.lock

@ -1,201 +0,0 @@
PODS:
- amap_service (0.0.1):
- AMapNavi (~> 10.0.900)
- Flutter
- AMapFoundation (1.8.2)
- AMapNavi (10.0.900):
- AMapFoundation (>= 1.8.2)
- audio_waveforms (0.0.1):
- Flutter
- classic_bluetooth (0.0.1):
- Flutter
- FBAEMKit (18.0.0):
- FBSDKCoreKit_Basics (= 18.0.0)
- FBSDKCoreKit (18.0.0):
- FBAEMKit (= 18.0.0)
- FBSDKCoreKit_Basics (= 18.0.0)
- FBSDKCoreKit_Basics (18.0.0)
- FBSDKLoginKit (18.0.0):
- FBSDKCoreKit (= 18.0.0)
- Flutter (1.0.0)
- flutter_facebook_auth (7.1.2):
- FBSDKLoginKit (~> 18.0.0)
- Flutter
- flutter_image_compress_common (1.0.0):
- Flutter
- Mantle
- SDWebImage
- SDWebImageWebPCoder
- flutter_local_notifications (0.0.1):
- Flutter
- flutter_secure_storage (6.0.0):
- Flutter
- flutter_tts (0.0.1):
- Flutter
- fluwx (0.0.1):
- Flutter
- fluwx/pay (= 0.0.1)
- fluwx/pay (0.0.1):
- Flutter
- WechatOpenSDK-XCFramework (~> 2.0.4)
- haptic_feedback (0.5.1):
- Flutter
- keyboard_height_plugin (0.0.1):
- Flutter
- libwebp (1.5.0):
- libwebp/demux (= 1.5.0)
- libwebp/mux (= 1.5.0)
- libwebp/sharpyuv (= 1.5.0)
- libwebp/webp (= 1.5.0)
- libwebp/demux (1.5.0):
- libwebp/webp
- libwebp/mux (1.5.0):
- libwebp/demux
- libwebp/sharpyuv (1.5.0)
- libwebp/webp (1.5.0):
- libwebp/sharpyuv
- Mantle (2.2.0):
- Mantle/extobjc (= 2.2.0)
- Mantle/extobjc (2.2.0)
- ota (0.0.1):
- Flutter
- permission_handler_apple (9.3.0):
- Flutter
- QCloudCore (6.4.7):
- QCloudCore/Default (= 6.4.7)
- QCloudCore/Default (6.4.7):
- QCloudTrack/Beacon (= 6.4.7)
- QCloudCOSXML (6.4.7):
- QCloudCOSXML/Default (= 6.4.7)
- QCloudCOSXML/Default (6.4.7):
- QCloudCore (= 6.4.7)
- QCloudTrack/Beacon (6.4.7)
- record_darwin (1.0.0):
- Flutter
- SDWebImage (5.21.1):
- SDWebImage/Core (= 5.21.1)
- SDWebImage/Core (5.21.1)
- SDWebImageWebPCoder (0.14.6):
- libwebp (~> 1.0)
- SDWebImage/Core (~> 5.17)
- spotify_sdk (0.0.1):
- Flutter
- tencentcloud_cos_sdk_plugin (1.2.3):
- Flutter
- QCloudCOSXML (= 6.4.7)
- WechatOpenSDK-XCFramework (2.0.4)
- wifi_iot (0.0.1):
- Flutter
DEPENDENCIES:
- amap_service (from `.symlinks/plugins/amap_service/ios`)
- audio_waveforms (from `.symlinks/plugins/audio_waveforms/ios`)
- classic_bluetooth (from `.symlinks/plugins/classic_bluetooth/ios`)
- Flutter (from `Flutter`)
- flutter_facebook_auth (from `.symlinks/plugins/flutter_facebook_auth/ios`)
- flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- flutter_tts (from `.symlinks/plugins/flutter_tts/ios`)
- fluwx (from `.symlinks/plugins/fluwx/ios`)
- haptic_feedback (from `.symlinks/plugins/haptic_feedback/ios`)
- keyboard_height_plugin (from `.symlinks/plugins/keyboard_height_plugin/ios`)
- ota (from `.symlinks/plugins/ota/ios`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- record_darwin (from `.symlinks/plugins/record_darwin/ios`)
- spotify_sdk (from `.symlinks/plugins/spotify_sdk/ios`)
- tencentcloud_cos_sdk_plugin (from `.symlinks/plugins/tencentcloud_cos_sdk_plugin/ios`)
- wifi_iot (from `.symlinks/plugins/wifi_iot/ios`)
SPEC REPOS:
trunk:
- AMapFoundation
- AMapNavi
- FBAEMKit
- FBSDKCoreKit
- FBSDKCoreKit_Basics
- FBSDKLoginKit
- libwebp
- Mantle
- QCloudCore
- QCloudCOSXML
- QCloudTrack
- SDWebImage
- SDWebImageWebPCoder
- WechatOpenSDK-XCFramework
EXTERNAL SOURCES:
amap_service:
:path: ".symlinks/plugins/amap_service/ios"
audio_waveforms:
:path: ".symlinks/plugins/audio_waveforms/ios"
classic_bluetooth:
:path: ".symlinks/plugins/classic_bluetooth/ios"
Flutter:
:path: Flutter
flutter_facebook_auth:
:path: ".symlinks/plugins/flutter_facebook_auth/ios"
flutter_image_compress_common:
:path: ".symlinks/plugins/flutter_image_compress_common/ios"
flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_secure_storage:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
flutter_tts:
:path: ".symlinks/plugins/flutter_tts/ios"
fluwx:
:path: ".symlinks/plugins/fluwx/ios"
haptic_feedback:
:path: ".symlinks/plugins/haptic_feedback/ios"
keyboard_height_plugin:
:path: ".symlinks/plugins/keyboard_height_plugin/ios"
ota:
:path: ".symlinks/plugins/ota/ios"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
record_darwin:
:path: ".symlinks/plugins/record_darwin/ios"
spotify_sdk:
:path: ".symlinks/plugins/spotify_sdk/ios"
tencentcloud_cos_sdk_plugin:
:path: ".symlinks/plugins/tencentcloud_cos_sdk_plugin/ios"
wifi_iot:
:path: ".symlinks/plugins/wifi_iot/ios"
SPEC CHECKSUMS:
amap_service: 5d40a69bd91031df3da423894b75a0236e26da96
AMapFoundation: 9885c48fc3a78fdfb84a0299a2293e56ea3c9fec
AMapNavi: fceb4715f9a3b5a25f330facebee2aa2bdba42dd
audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf
classic_bluetooth: 29f392f1ab7c52862a9ef036c94f2713b02e0c50
FBAEMKit: e34530df538b8eb8aeb53c35867715ba6c63ef0c
FBSDKCoreKit: d3f479a69127acebb1c6aad91c1a33907bcf6c2f
FBSDKCoreKit_Basics: 017b6dc2a1862024815a8229e75661e627ac1e29
FBSDKLoginKit: 5875762d1fe09ddcb05d03365d4f5dc34413843d
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_facebook_auth: d97dc09216aebd99d7ac52360186a630f67bdf18
flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
flutter_local_notifications: 395056b3175ba4f08480a7c5de30cd36d69827e4
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
flutter_tts: b88dbc8655d3dc961bc4a796e4e16a4cc1795833
fluwx: 6bf9c5a3a99ad31b0de137dd92370a0d10a60f4b
haptic_feedback: fd1d8509833f58f164fc12122c0357fa9b2750e0
keyboard_height_plugin: ef70a8181b29f27670e9e2450814ca6b6dc05b05
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
ota: 2256716620ec8ecfc2aea220679557f94b13915f
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
QCloudCore: 49e7571984a8e5fd8f0fd58c2f3b806255796223
QCloudCOSXML: 7205e76aa9cf613468222615483c9d7c074e4298
QCloudTrack: 3b53a7fc4fe3920e407f2aa73f2452992a61f7f3
record_darwin: fb1f375f1d9603714f55b8708a903bbb91ffdb0a
SDWebImage: f29024626962457f3470184232766516dee8dfea
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
spotify_sdk: 60c0a0c8d154f2c67f5598f2d652c3877e2b570a
tencentcloud_cos_sdk_plugin: c3d78b2c3bae42b5a20009dc047f3f1621e6272b
WechatOpenSDK-XCFramework: 36fb2bea0754266c17184adf4963d7e6ff98b69f
wifi_iot: f645260a2be8608517b2a9bf4c39b98e97003acc
PODFILE CHECKSUM: e69e629768e642fde97676ea149350c61d57751e
COCOAPODS: 1.16.2

2
lib/core/bindings/initial_binding.dart

@ -1,4 +1,4 @@
import 'package:deep_voice/data/services/user_portrait.dart';
import '../../../data/services/user_portrait.dart';
import '../../../data/services/location_manager.dart';
import '../../../data/services/music_manager.dart';

56
lib/core/translations/language/en_us.dart

@ -522,6 +522,47 @@ const Map<String, String> enUS = {
'You have permanently denied {permission} permission, please go to app settings to enable it',
'goToSettings': 'Go to Settings',
// 权限申请
'bluetoothScan': 'Bluetooth Scan',
'bluetoothConnect': 'Bluetooth Connect',
'bluetoothScanExplanation':
'The app needs Bluetooth scan permission to scan nearby Bluetooth devices.',
'bluetoothConnectExplanation':
'The app needs Bluetooth connect permission to connect to Bluetooth devices.',
'locationForBluetoothExplanation':
'The app needs location permission to scan Bluetooth devices.',
'bluetoothScanPermanentDenial':
'Bluetooth scan permission has been permanently denied, please go to settings to enable it manually.',
'bluetoothConnectPermanentDenial':
'Bluetooth connect permission has been permanently denied, please go to settings to enable it manually.',
'notificationPermission': 'Notification',
'locationPermission': 'Location',
'notificationPermissionExplanation':
'Notification permission is required to display music playback status.',
'locationPermissionExplanation':
'Location permission is required to get current location.',
'notificationPermissionPermanentDenial':
'You have permanently denied notification permission, please go to settings to enable it.',
'locationPermissionPermanentDenial':
'You have permanently denied location permission, please go to settings to enable it.',
//'microphonePermission': 'Microphone',
'storagePermission': 'Storage',
'microphonePermissionExplanation':
'We need microphone permission to record audio content.',
'storagePermissionExplanation':
'We need storage permission so you can select and upload audio files.',
'microphonePermissionPermanentDenial':
'Microphone permission has been permanently denied. To continue using this feature, please go to app settings to manually enable microphone permission.',
'storagePermissionPermanentDenial':
'Storage permission has been permanently denied. To continue using this feature, please go to app settings to manually enable storage permission.',
//permission_util.dart
'permissionUtilNeed': 'Need ',
'permissionUtilPermission': ' Permission',
'permissionUtilRestricted': ' Permission Restricted',
'permissionUtilRestrictedMessage':
'This feature is unavailable or restricted on the current device, please check device settings or system restrictions',
'permissionUtilConfirm': 'OK',
// 版本更新对话框
'newVersionFound': 'New Version Found', // 版本更新对话框标题
'remindLater': 'Remind Later', // 稍后提醒按钮
@ -764,7 +805,8 @@ const Map<String, String> enUS = {
'invalidDeviceAddress': 'Invalid device address',
'bleDeviceAssociationFailed': 'BLE device association failed',
'deviceAssociationFailedRetry': 'Device association failed, please retry',
'bleConnectionTimeoutRetry': 'BLE connection timeout, please retry',
'bleConnectionTimeoutRetry':
'BLE connection timeout. Please restart the earphone and try again',
'blePairingStartupFailed': 'BLE pairing startup failed',
'bleConnectionFailed': 'BLE connection failed',
'deviceAuthorizationFailedCheckCode':
@ -851,4 +893,16 @@ const Map<String, String> enUS = {
"乌尔都语": "Urdu",
"波斯语": "Persian",
"斯瓦希里语": "Swahili",
// Time related
"justNow": "Just now",
"minutesAgo": "@minutes minutes ago",
"yesterday": "Yesterday",
// "monday": "Monday",
// "tuesday": "Tuesday",
// "wednesday": "Wednesday",
// "thursday": "Thursday",
// "friday": "Friday",
// "saturday": "Saturday",
// "sunday": "Sunday",
};

38
lib/core/translations/language/zh_cn.dart

@ -482,6 +482,37 @@ const Map<String, String> zhCN = {
'permanentDenialText': '您已永久拒绝{permission}权限,请前往应用设置开启',
'goToSettings': '去设置',
// 权限申请
'bluetoothScan': '蓝牙扫描',
'bluetoothConnect': '蓝牙连接',
'bluetoothScanExplanation': '为了扫描附近的蓝牙设备,应用需要获取蓝牙扫描权限。',
'bluetoothConnectExplanation': '为了连接蓝牙设备,应用需要获取蓝牙连接权限。',
'locationForBluetoothExplanation': '为了扫描蓝牙设备,应用需要获取位置信息权限。',
'bluetoothScanPermanentDenial': '蓝牙扫描权限已被永久拒绝,请前往设置手动开启。',
'bluetoothConnectPermanentDenial': '蓝牙连接权限已被永久拒绝,请前往设置手动开启。',
'locationPermanentDenial': '位置信息权限已被永久拒绝,请前往设置手动开启。',
'notificationPermission': '通知',
'locationPermission': '定位',
'notificationPermissionExplanation': '需要通知权限来显示音乐播放状态。',
'locationPermissionExplanation': '需要定位权限来获取当前位置。',
'notificationPermissionPermanentDenial': '您已永久拒绝通知权限,请前往设置开启权限。',
'locationPermissionPermanentDenial': '您已永久拒绝定位权限,请前往设置开启权限。',
//'microphonePermission': '录音',
'storagePermission': '存储',
'microphonePermissionExplanation': '我们需要访问麦克风权限,以便您录制音频内容。',
'storagePermissionExplanation': '我们需要访问存储权限,以便您可以选择并上传音频文件。',
'microphonePermissionPermanentDenial':
'检测到您已永久拒绝录音权限。如需继续使用此功能,请前往应用设置中手动开启麦克风权限。',
'storagePermissionPermanentDenial':
'检测到您已永久拒绝存储权限。如需继续使用此功能,请前往应用设置中手动开启存储权限。',
//permission_util.dart
'permissionUtilNeed': '需要',
'permissionUtilPermission': '权限',
'permissionUtilRestricted': '权限受限',
'permissionUtilRestrictedMessage': '此功能在当前设备上不可用或受限,请检查设备设置或系统限制',
'permissionUtilConfirm': '确定',
// 版本更新对话框
'newVersionFound': '发现新版本', // 版本更新对话框标题
'remindLater': '稍后提醒', // 稍后提醒按钮
@ -710,7 +741,7 @@ const Map<String, String> zhCN = {
'invalidDeviceAddress': '设备地址无效',
'bleDeviceAssociationFailed': 'BLE设备关联失败',
'deviceAssociationFailedRetry': '设备关联失败,请重试',
'bleConnectionTimeoutRetry': 'BLE连接超时,请重试',
'bleConnectionTimeoutRetry': 'BLE连接超时,请重启耳机后再试',
'blePairingStartupFailed': 'BLE配对启动失败',
'bleConnectionFailed': 'BLE连接失败',
'deviceAuthorizationFailedCheckCode': '设备授权失败,请检查授权码',
@ -793,4 +824,9 @@ const Map<String, String> zhCN = {
"乌尔都语": "乌尔都语",
"波斯语": "波斯语",
"斯瓦希里语": "斯瓦希里语",
// 时间相关
"justNow": "刚刚",
"minutesAgo": "@minutes分钟前",
"yesterday": "昨天",
};

27
lib/core/utils/permission_util.dart

@ -42,9 +42,14 @@ class PermissionUtil {
required String permissionName,
required String explanationText,
required String permanentDenialText,
String settingsButtonText = '前往设置',
String cancelButtonText = '取消',
String? settingsButtonText,
String? cancelButtonText,
}) async {
// 使用翻译键作为默认值
final String finalSettingsButtonText =
settingsButtonText ?? 'goToSettings'.tr;
final String finalCancelButtonText = cancelButtonText ?? 'cancel'.tr;
try {
// 检查初始权限状态
final PermissionStatus status = await permissionType.status;
@ -73,8 +78,8 @@ class PermissionUtil {
// context: context,
permissionName: permissionName,
permanentDenialText: permanentDenialText,
settingsButtonText: settingsButtonText,
cancelButtonText: cancelButtonText,
settingsButtonText: finalSettingsButtonText,
cancelButtonText: finalCancelButtonText,
);
// 移除解释横幅
_hideExplanationOverlay();
@ -109,8 +114,8 @@ class PermissionUtil {
// context: context,
permissionName: permissionName,
permanentDenialText: permanentDenialText,
settingsButtonText: settingsButtonText,
cancelButtonText: cancelButtonText,
settingsButtonText: finalSettingsButtonText,
cancelButtonText: finalCancelButtonText,
);
_hideExplanationOverlay();
return result;
@ -171,7 +176,7 @@ class PermissionUtil {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'需要$permissionName权限',
'permissionUtilNeed'.tr + permissionName + 'permissionUtilPermission'.tr,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
@ -256,7 +261,7 @@ class PermissionUtil {
bool? userChoice = await Get.dialog(
AlertDialog(
title: Text('需要$permissionName权限'),
title: Text('permissionUtilNeed'.tr + permissionName + 'permissionUtilPermission'.tr),
content: Text(permanentDenialText),
actions: [
TextButton(
@ -301,8 +306,8 @@ class PermissionUtil {
Logger.w('PermissionUtil', '显示受限权限对话框 ($permissionName)');
Get.dialog(
AlertDialog(
title: Text('$permissionName权限受限'),
content: const Text('此功能在当前设备上不可用或受限,请检查设备设置或系统限制'),
title: Text(permissionName + 'permissionUtilRestricted'.tr),
content: Text('permissionUtilRestrictedMessage'.tr),
actions: [
TextButton(
onPressed: () {
@ -310,7 +315,7 @@ class PermissionUtil {
// Navigator.pop(context);
Get.back();
},
child: const Text('确定'),
child: Text('permissionUtilConfirm'.tr),
),
],
),

12
lib/data/models/appconfig_model.dart

@ -71,14 +71,24 @@ class DBMCPServer {
@JsonSerializable()
class DBProduct {
final String id;
final int id;
final int factoryid;
final int devicetype;
final String devicename;
final String scanuuid;
final String version;
final String updatedescription;
final String updatepackageaddress;
DBProduct({
required this.id,
required this.factoryid,
required this.devicetype,
required this.devicename,
required this.scanuuid,
required this.version,
required this.updatedescription,
required this.updatepackageaddress,
});
factory DBProduct.fromJson(Map<String, dynamic> json) =>

7
lib/data/models/appconfig_model.g.dart

@ -72,9 +72,14 @@ Map<String, dynamic> _$DBMCPServerToJson(DBMCPServer instance) =>
};
DBProduct _$DBProductFromJson(Map<String, dynamic> json) => DBProduct(
id: json['id'] as String,
id: json['id'] as int,
factoryid: (json['factoryid'] as num).toInt(),
devicename: json['devicename'] as String,
devicetype: json['devicetype'] as int,
scanuuid: json['scanuuid'] as String,
version: json['version'] as String,
updatedescription: json['updatedescription'] as String,
updatepackageaddress: json['updatepackageaddress'] as String,
);
Map<String, dynamic> _$DBProductToJson(DBProduct instance) => <String, dynamic>{

2
lib/data/models/channel_app_model.dart

@ -74,7 +74,7 @@ class AppInfo {
required this.channel,
required this.description,
required this.version,
required this.tourists,
this.tourists = false,
});
factory AppInfo.fromJson(Map<String, dynamic> json) =>

2
lib/data/models/channel_app_model.g.dart

@ -37,7 +37,7 @@ AppInfo _$AppInfoFromJson(Map<String, dynamic> json) => AppInfo(
channel: UserChannel.fromJson(json['channel']),
description: json['description'] as String,
version: json['version'] as String,
tourists: json['tourists'] as bool,
tourists: json['tourists'] as bool? ?? false,
);
Map<String, dynamic> _$AppInfoToJson(AppInfo instance) => <String, dynamic>{

450
lib/data/models/translation_usage_stats.dart

@ -0,0 +1,450 @@
/// 翻译模式统计数据
class TranslationModeStats {
final int apiCalls; // API调用次数
final int sourceCharacters; // 源文本字符数
final int effectiveAudioDuration; // 有效音频时长
final int ttsCharacters; // TTS字符数
final double cost; // 成本
TranslationModeStats({
this.apiCalls = 0,
this.sourceCharacters = 0,
this.effectiveAudioDuration = 0,
this.ttsCharacters = 0,
this.cost = 0.0,
});
factory TranslationModeStats.fromJson(Map<String, dynamic> json) {
return TranslationModeStats(
apiCalls: json['apiCalls'] ?? 0,
sourceCharacters: json['sourceCharacters'] ?? 0,
effectiveAudioDuration: json['effectiveAudioDuration'] ?? 0,
ttsCharacters: json['ttsCharacters'] ?? 0,
cost: (json['cost'] ?? 0.0).toDouble(),
);
}
Map<String, dynamic> toJson() {
return {
'apiCalls': apiCalls,
'sourceCharacters': sourceCharacters,
'effectiveAudioDuration': effectiveAudioDuration,
'ttsCharacters': ttsCharacters,
'cost': cost,
};
}
TranslationModeStats copyWith({
int? apiCalls,
int? sourceCharacters,
int? effectiveAudioDuration,
int? ttsCharacters,
double? cost,
}) {
return TranslationModeStats(
apiCalls: apiCalls ?? this.apiCalls,
sourceCharacters: sourceCharacters ?? this.sourceCharacters,
effectiveAudioDuration: effectiveAudioDuration ?? this.effectiveAudioDuration,
ttsCharacters: ttsCharacters ?? this.ttsCharacters,
cost: cost ?? this.cost,
);
}
}
/// 语言对统计数据
class LanguagePairStats {
final String languagePair; // 语言对,如"zh-CN→en-US"
final int translationCount; // 翻译次数
final int sourceCharacters; // 源文本字符数
final double cost; // 成本
LanguagePairStats({
required this.languagePair,
this.translationCount = 0,
this.sourceCharacters = 0,
this.cost = 0.0,
});
factory LanguagePairStats.fromJson(Map<String, dynamic> json) {
return LanguagePairStats(
languagePair: json['languagePair'] ?? '',
translationCount: json['translationCount'] ?? 0,
sourceCharacters: json['sourceCharacters'] ?? 0,
cost: (json['cost'] ?? 0.0).toDouble(),
);
}
Map<String, dynamic> toJson() {
return {
'languagePair': languagePair,
'translationCount': translationCount,
'sourceCharacters': sourceCharacters,
'cost': cost,
};
}
}
/// 小时统计数据
class HourlyStats {
final int hour; // 小时(0-23)
final int apiCalls; // API调用次数
final int sourceCharacters; // 源文本字符数
final int effectiveAudioDuration; // 有效音频时长
final double cost; // 成本
HourlyStats({
required this.hour,
this.apiCalls = 0,
this.sourceCharacters = 0,
this.effectiveAudioDuration = 0,
this.cost = 0.0,
});
factory HourlyStats.fromJson(Map<String, dynamic> json) {
return HourlyStats(
hour: json['hour'] ?? 0,
apiCalls: json['apiCalls'] ?? 0,
sourceCharacters: json['sourceCharacters'] ?? 0,
effectiveAudioDuration: json['effectiveAudioDuration'] ?? 0,
cost: (json['cost'] ?? 0.0).toDouble(),
);
}
Map<String, dynamic> toJson() {
return {
'hour': hour,
'apiCalls': apiCalls,
'sourceCharacters': sourceCharacters,
'effectiveAudioDuration': effectiveAudioDuration,
'cost': cost,
};
}
}
/// 翻译使用统计数据模型 - 优化版
class TranslationUsageStats {
// 基础信息
final String id; // 统计记录ID
final DateTime date; // 统计日期
final String userId; // 用户ID
final String timeGranularity; // 时间粒度:realtime/hour/day/week/month
// API调用统计 - 精确化
final int translationApiCalls; // 翻译API调用次数
final int asrApiCalls; // ASR语音识别调用次数
final int ttsApiCalls; // TTS语音合成调用次数
final int aiSummaryApiCalls; // AI摘要生成调用次数(已废弃但保留兼容性)
// 精确计费参数统计
final int sourceTextCharacters; // 源文本字符数(翻译API计费基础)
final int effectiveAudioDuration; // ASR有效语音时长(秒,ASR计费基础)
final int ttsTextCharacters; // TTS文本字符数(TTS计费基础)
final int totalSessionDuration; // 总会话时长(秒,用于分析)
// 网络流量统计
final int asrNetworkBytes; // ASR网络流量(字节)
final int ttsNetworkBytes; // TTS网络流量(字节)
final int translationNetworkBytes; // 翻译API网络流量(字节)
// 多维度分类统计
final int sessionCount; // 翻译会话数量
final Map<String, TranslationModeStats> modeStats; // 按翻译模式分类统计
final Map<String, LanguagePairStats> languagePairStats; // 按语言对分类统计
final Map<String, HourlyStats> hourlyStats; // 按小时分类统计(仅日统计包含)
// 成本相关
final double estimatedCost; // 预估成本
final String currency; // 货币单位
final Map<String, double> costBreakdown; // 成本分解(ASR/TTS/Translation)
TranslationUsageStats({
required this.id,
required this.date,
required this.userId,
this.timeGranularity = 'day',
this.translationApiCalls = 0,
this.asrApiCalls = 0,
this.ttsApiCalls = 0,
this.aiSummaryApiCalls = 0,
this.sourceTextCharacters = 0,
this.effectiveAudioDuration = 0,
this.ttsTextCharacters = 0,
this.totalSessionDuration = 0,
this.asrNetworkBytes = 0,
this.ttsNetworkBytes = 0,
this.translationNetworkBytes = 0,
this.sessionCount = 0,
this.modeStats = const {},
this.languagePairStats = const {},
this.hourlyStats = const {},
this.estimatedCost = 0.0,
this.currency = 'CNY',
this.costBreakdown = const {},
});
factory TranslationUsageStats.fromJson(Map<String, dynamic> json) {
// 处理多维度统计数据的反序列化
final modeStatsMap = <String, TranslationModeStats>{};
if (json['modeStats'] != null) {
final modeStatsJson = json['modeStats'] as Map<String, dynamic>;
modeStatsJson.forEach((key, value) {
modeStatsMap[key] = TranslationModeStats.fromJson(value);
});
}
final languagePairStatsMap = <String, LanguagePairStats>{};
if (json['languagePairStats'] != null) {
final languagePairStatsJson = json['languagePairStats'] as Map<String, dynamic>;
languagePairStatsJson.forEach((key, value) {
languagePairStatsMap[key] = LanguagePairStats.fromJson(value);
});
}
final hourlyStatsMap = <String, HourlyStats>{};
if (json['hourlyStats'] != null) {
final hourlyStatsJson = json['hourlyStats'] as Map<String, dynamic>;
hourlyStatsJson.forEach((key, value) {
hourlyStatsMap[key] = HourlyStats.fromJson(value);
});
}
return TranslationUsageStats(
id: json['id'] ?? '',
date: DateTime.parse(json['date']),
userId: json['userId'] ?? '',
timeGranularity: json['timeGranularity'] ?? 'day',
translationApiCalls: json['translationApiCalls'] ?? 0,
asrApiCalls: json['asrApiCalls'] ?? 0,
ttsApiCalls: json['ttsApiCalls'] ?? 0,
aiSummaryApiCalls: json['aiSummaryApiCalls'] ?? 0,
sourceTextCharacters: json['sourceTextCharacters'] ?? json['sourceCharacterCount'] ?? 0, // 兼容旧字段
effectiveAudioDuration: json['effectiveAudioDuration'] ?? json['audioRecordingDuration'] ?? 0, // 兼容旧字段
ttsTextCharacters: json['ttsTextCharacters'] ?? json['targetCharacterCount'] ?? 0, // 兼容旧字段
totalSessionDuration: json['totalSessionDuration'] ?? 0,
asrNetworkBytes: json['asrNetworkBytes'] ?? 0,
ttsNetworkBytes: json['ttsNetworkBytes'] ?? 0,
translationNetworkBytes: json['translationNetworkBytes'] ?? 0,
sessionCount: json['sessionCount'] ?? 0,
modeStats: modeStatsMap,
languagePairStats: languagePairStatsMap,
hourlyStats: hourlyStatsMap,
estimatedCost: (json['estimatedCost'] ?? 0.0).toDouble(),
currency: json['currency'] ?? 'CNY',
costBreakdown: Map<String, double>.from(json['costBreakdown'] ?? {}),
);
}
Map<String, dynamic> toJson() {
// 序列化多维度统计数据
final modeStatsJson = <String, dynamic>{};
modeStats.forEach((key, value) {
modeStatsJson[key] = value.toJson();
});
final languagePairStatsJson = <String, dynamic>{};
languagePairStats.forEach((key, value) {
languagePairStatsJson[key] = value.toJson();
});
final hourlyStatsJson = <String, dynamic>{};
hourlyStats.forEach((key, value) {
hourlyStatsJson[key] = value.toJson();
});
return {
'id': id,
'date': date.toIso8601String(),
'userId': userId,
'timeGranularity': timeGranularity,
'translationApiCalls': translationApiCalls,
'asrApiCalls': asrApiCalls,
'ttsApiCalls': ttsApiCalls,
'aiSummaryApiCalls': aiSummaryApiCalls,
'sourceTextCharacters': sourceTextCharacters,
'effectiveAudioDuration': effectiveAudioDuration,
'ttsTextCharacters': ttsTextCharacters,
'totalSessionDuration': totalSessionDuration,
'asrNetworkBytes': asrNetworkBytes,
'ttsNetworkBytes': ttsNetworkBytes,
'translationNetworkBytes': translationNetworkBytes,
'sessionCount': sessionCount,
'modeStats': modeStatsJson,
'languagePairStats': languagePairStatsJson,
'hourlyStats': hourlyStatsJson,
'estimatedCost': estimatedCost,
'currency': currency,
'costBreakdown': costBreakdown,
// 保持向后兼容性
'sourceCharacterCount': sourceTextCharacters,
'targetCharacterCount': ttsTextCharacters,
'audioRecordingDuration': effectiveAudioDuration,
'ttsPlaybackDuration': totalSessionDuration,
};
}
/// 创建空的统计记录
factory TranslationUsageStats.empty(String userId) {
return TranslationUsageStats(
id: DateTime.now().millisecondsSinceEpoch.toString(),
date: DateTime.now(),
userId: userId,
);
}
/// 复制并更新统计数据
TranslationUsageStats copyWith({
String? id,
DateTime? date,
String? userId,
String? timeGranularity,
int? translationApiCalls,
int? asrApiCalls,
int? ttsApiCalls,
int? aiSummaryApiCalls,
int? sourceTextCharacters,
int? effectiveAudioDuration,
int? ttsTextCharacters,
int? totalSessionDuration,
int? asrNetworkBytes,
int? ttsNetworkBytes,
int? translationNetworkBytes,
int? sessionCount,
Map<String, TranslationModeStats>? modeStats,
Map<String, LanguagePairStats>? languagePairStats,
Map<String, HourlyStats>? hourlyStats,
double? estimatedCost,
String? currency,
Map<String, double>? costBreakdown,
}) {
return TranslationUsageStats(
id: id ?? this.id,
date: date ?? this.date,
userId: userId ?? this.userId,
timeGranularity: timeGranularity ?? this.timeGranularity,
translationApiCalls: translationApiCalls ?? this.translationApiCalls,
asrApiCalls: asrApiCalls ?? this.asrApiCalls,
ttsApiCalls: ttsApiCalls ?? this.ttsApiCalls,
aiSummaryApiCalls: aiSummaryApiCalls ?? this.aiSummaryApiCalls,
sourceTextCharacters: sourceTextCharacters ?? this.sourceTextCharacters,
effectiveAudioDuration: effectiveAudioDuration ?? this.effectiveAudioDuration,
ttsTextCharacters: ttsTextCharacters ?? this.ttsTextCharacters,
totalSessionDuration: totalSessionDuration ?? this.totalSessionDuration,
asrNetworkBytes: asrNetworkBytes ?? this.asrNetworkBytes,
ttsNetworkBytes: ttsNetworkBytes ?? this.ttsNetworkBytes,
translationNetworkBytes: translationNetworkBytes ?? this.translationNetworkBytes,
sessionCount: sessionCount ?? this.sessionCount,
modeStats: modeStats ?? this.modeStats,
languagePairStats: languagePairStats ?? this.languagePairStats,
hourlyStats: hourlyStats ?? this.hourlyStats,
estimatedCost: estimatedCost ?? this.estimatedCost,
currency: currency ?? this.currency,
costBreakdown: costBreakdown ?? this.costBreakdown,
);
}
/// 获取总API调用次数
int get totalApiCalls => translationApiCalls + asrApiCalls + ttsApiCalls + aiSummaryApiCalls;
/// 获取总字符数(精确计费基础)
int get totalCharacterCount => sourceTextCharacters + ttsTextCharacters;
/// 获取总音频时长(包含有效时长和总会话时长)
int get totalAudioDuration => effectiveAudioDuration + totalSessionDuration;
/// 获取总网络流量
int get totalNetworkBytes => asrNetworkBytes + ttsNetworkBytes + translationNetworkBytes;
/// 获取最常用的语言对
String get mostUsedLanguagePair {
if (languagePairStats.isEmpty) return '无数据';
return languagePairStats.entries
.reduce((a, b) => a.value.translationCount > b.value.translationCount ? a : b)
.key;
}
/// 获取最常用的翻译模式
String get mostUsedMode {
if (modeStats.isEmpty) return '无数据';
return modeStats.entries
.reduce((a, b) => a.value.apiCalls > b.value.apiCalls ? a : b)
.key;
}
// 向后兼容性getter
/// @deprecated 使用 sourceTextCharacters 替代
int get sourceCharacterCount => sourceTextCharacters;
/// @deprecated 使用 ttsTextCharacters 替代
int get targetCharacterCount => ttsTextCharacters;
/// @deprecated 使用 effectiveAudioDuration 替代
int get audioRecordingDuration => effectiveAudioDuration;
/// @deprecated 使用 totalSessionDuration 替代
int get ttsPlaybackDuration => totalSessionDuration;
/// @deprecated 使用 languagePairStats 替代
Map<String, int> get languagePairUsage {
final result = <String, int>{};
languagePairStats.forEach((key, value) {
result[key] = value.translationCount;
});
return result;
}
/// @deprecated 使用 modeStats 替代
Map<String, int> get modeUsage {
final result = <String, int>{};
modeStats.forEach((key, value) {
result[key] = value.apiCalls;
});
return result;
}
}
/// 实时统计数据模型(用于当前会话)- 优化版
class RealtimeUsageStats {
// 精确计费参数
int translationCount = 0; // 当前会话翻译次数
int sourceTextCharacters = 0; // 当前会话源文本字符数(翻译计费基础)
int ttsTextCharacters = 0; // 当前会话TTS文本字符数(TTS计费基础)
int effectiveAudioDuration = 0; // 当前会话有效语音时长(ASR计费基础)
int totalSessionDuration = 0; // 当前会话总时长
// 会话信息
String currentLanguagePair = ''; // 当前语言对
String currentMode = ''; // 当前翻译模式
DateTime sessionStartTime = DateTime.now(); // 会话开始时间
/// 重置统计数据
void reset() {
translationCount = 0;
sourceTextCharacters = 0;
ttsTextCharacters = 0;
effectiveAudioDuration = 0;
totalSessionDuration = 0;
currentLanguagePair = '';
currentMode = '';
sessionStartTime = DateTime.now();
}
/// 获取会话持续时间(分钟)
int get sessionDurationMinutes {
return DateTime.now().difference(sessionStartTime).inMinutes;
}
// 向后兼容性getter
/// @deprecated 使用 sourceTextCharacters 替代
int get sourceCharacters => sourceTextCharacters;
/// @deprecated 使用 ttsTextCharacters 替代
int get targetCharacters => ttsTextCharacters;
/// @deprecated 使用 effectiveAudioDuration 替代
int get recordingDuration => effectiveAudioDuration;
/// @deprecated 使用 totalSessionDuration 替代
int get ttsPlayDuration => totalSessionDuration;
}

9
lib/data/models/user_Info.dart

@ -23,6 +23,7 @@ class User {
String phonemac;
String password;
int viplv = 0; //添加会员等级字段,0表示普通用户
List<UserDevice> devices;
// 私有构造函数
@ -43,7 +44,7 @@ class User {
factory User.fromJson(Map<String, dynamic> json) {
// 修复1:安全获取嵌套的 user 字段
final userData = json['user'] as Map<String, dynamic>?; // 允许空值
final devicesData = json['devices'] as Map<String, dynamic>?; // 允许空值
final devicesData = json['devices'] as List<dynamic>?; // 允许空值
//final token = json['data']['token'] as String? ?? '';
//final token = '';
// 更新单例实例
@ -61,7 +62,7 @@ class User {
phonemac: (userData?['phonemac'] as String?) ?? '',
password: (userData?['password'] as String?) ?? '',
viplv: (userData?['viplv'] as int?) ?? 0, // 提供默认值
devices: (devicesData as List<dynamic>?)
devices: devicesData
?.map((e) => UserDevice.fromJson(e as Map<String, dynamic>))
.toList() ??
[], // 处理设备列表
@ -70,7 +71,7 @@ class User {
factory User.fronTokenJson(Map<String, dynamic> json) {
// 修复1:安全获取嵌套的 user 字段
final userData = json['user'] as Map<String, dynamic>?; // 允许空值
final devicesData = json['devices'] as Map<String, dynamic>?; // 允许空值
final devicesData = json['devices'] as List<dynamic>?; // 允许空值
print("数据获取fronTokenJson${userData} ${devicesData}");
// 更新单例实例
return _instance = User._(
@ -86,7 +87,7 @@ class User {
password: (userData?['password'] as String?) ?? '',
viplv: (userData?['viplv'] as int?) ?? 0, // 提供默认值
devices: devicesData != null
? (devicesData as List<dynamic>)
? (devicesData)
.map((e) => UserDevice.fromJson(e as Map<String, dynamic>))
.toList()
: [], // 处理设备列表

19
lib/data/services/db/sqflite_api.dart

@ -6,27 +6,12 @@ import 'database_helper.dart';
class SqfliteApi {
// 获取商务会议助手列表
static Future getMeetingList(int offset) async {
static Future getMeetingList() async {
final db = await DatabaseHelper().database;
List<Map<String, dynamic>> dataList = [];
List meetingList = await db.query(
return await db.query(
'meeting',
limit: 10,
offset: offset,
orderBy: 'creationtime DESC',
);
for (var element in meetingList) {
final List detailsList = await db.query(
'meetingdetails',
where: 'meetingid = ?',
whereArgs: [element['id']],
);
dataList.add({
...element,
'taskid': detailsList.first['taskid'],
});
}
return dataList;
}
// 获取商务会议助手详情

10
lib/data/services/location_manager.dart

@ -23,11 +23,11 @@ class LocationManager extends GetxService {
final granted = await PermissionUtil.instance.requestPermission(
// context: context,
permissionType: Permission.locationAlways,
permissionName: '通知',
explanationText: '需要定位权限来获取当前位置。',
permanentDenialText: '您已永久定位通知权限,请前往设置开启权限。',
settingsButtonText: '去设置',
cancelButtonText: '取消',
permissionName: 'locationPermission'.tr,
explanationText: 'locationPermissionExplanation'.tr,
permanentDenialText: 'locationPermissionPermanentDenial'.tr,
settingsButtonText: 'goToSettings'.tr,
cancelButtonText: 'cancel'.tr,
);
if (!granted) {

10
lib/data/services/meeting/meeting_task_service.dart

@ -33,7 +33,6 @@ class MeetingTaskService extends GetxService {
int meetingId = 0;
RxInt tasktype = 1.obs;
List textList = [];
String allText = '';
String summary = '';
String overview = '';
@ -268,11 +267,10 @@ class MeetingTaskService extends GetxService {
_storage.write("meeting_task_list", _taskList);
_storage.write("meeting_task_finish_list", taskFinishList);
}
allText = text;
_aiServiceMessage(id);
aiServiceMessage(id, text);
}
void _aiServiceMessage(int id) {
void aiServiceMessage(int id, String text) {
int awaitQuantity = 0;
String summaryText = '';
String overviewText = '';
@ -283,7 +281,7 @@ class MeetingTaskService extends GetxService {
'content':
'您将扮演一名会议总结专家,拥有超强的信息归纳与细分能力。不仅能高效处理各类通用场景会议,精准提炼会议核心内容,为后续思维导图生成奠定坚实基础。若涉及人员,人员名称保留[]。并根据内容只输出对应语言。',
},
{'role': 'user', 'content': allText},
{'role': 'user', 'content': text},
],
).then((value) {
awaitQuantity++;
@ -298,7 +296,7 @@ class MeetingTaskService extends GetxService {
'role': 'system',
'content': '您将扮演一名会议事件归纳专家,能准确抓住会议中事件的关键点,并每个事件只需一句话精简总结。',
},
{'role': 'user', 'content': allText},
{'role': 'user', 'content': text},
],
).then((value) {
awaitQuantity++;

12
lib/data/services/music_manager.dart

@ -28,13 +28,13 @@ class MusicManager extends GetxService {
Future<void> requestNotificationPermission() async {
final granted = await PermissionUtil.instance.requestPermission(
// context: context,
//context: context,
permissionType: Permission.notification,
permissionName: '通知',
explanationText: '需要通知权限来显示音乐播放状态。',
permanentDenialText: '您已永久拒绝通知权限,请前往设置开启权限。',
settingsButtonText: '去设置',
cancelButtonText: '取消',
permissionName: 'notificationPermission'.tr,
explanationText: 'notificationPermissionExplanation'.tr,
permanentDenialText: 'notificationPermissionPermanentDenial'.tr,
settingsButtonText: 'goToSettings'.tr,
cancelButtonText: 'cancel'.tr,
);
if (!granted) {

519
lib/data/services/translation_usage_service.dart

@ -0,0 +1,519 @@
import 'dart:convert';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import '../models/translation_usage_stats.dart';
import '../../core/utils/logger.dart';
/// 翻译使用量统计服务
class TranslationUsageService extends GetxService {
static const String _storageKey = 'translation_usage_stats';
static const String _dailyStatsKey = 'daily_translation_stats';
static const String _costConfigKey = 'translation_cost_config';
final GetStorage _storage = GetStorage();
// 实时统计数据
final Rx<RealtimeUsageStats> realtimeStats = RealtimeUsageStats().obs;
// 当日统计数据
final Rx<TranslationUsageStats> todayStats = TranslationUsageStats.empty('').obs;
// 成本配置
final RxMap<String, double> costConfig = <String, double>{}.obs;
// 预警阈值
final RxDouble dailyCostThreshold = 10.0.obs; // 日成本预警阈值
final RxDouble monthlyCostThreshold = 300.0.obs; // 月成本预警阈值
@override
void onInit() {
super.onInit();
_initializeService();
}
/// 初始化服务
void _initializeService() {
_loadCostConfig();
_loadTodayStats();
Logger.info('翻译使用量统计服务初始化完成');
}
/// 加载成本配置
void _loadCostConfig() {
try {
final configData = _storage.read(_costConfigKey);
if (configData != null) {
costConfig.value = Map<String, double>.from(configData);
} else {
// 设置默认成本配置(每千字符的成本,单位:元)
costConfig.value = {
'translation_per_1k_chars': 0.05, // 翻译:每千字符0.05元
'asr_per_minute': 0.02, // ASR:每分钟0.02元
'tts_per_1k_chars': 0.03, // TTS:每千字符0.03元
'ai_summary_per_request': 0.01, // AI摘要:每次请求0.01元
};
_saveCostConfig();
}
} catch (e) {
Logger.error('加载成本配置失败: $e');
}
}
/// 保存成本配置
void _saveCostConfig() {
try {
_storage.write(_costConfigKey, costConfig.value);
} catch (e) {
Logger.error('保存成本配置失败: $e');
}
}
/// 加载当日统计数据
void _loadTodayStats() {
try {
final today = DateTime.now();
final todayKey = '${today.year}-${today.month.toString().padLeft(2, '0')}-${today.day.toString().padLeft(2, '0')}';
final statsData = _storage.read('${_dailyStatsKey}_$todayKey');
if (statsData != null) {
todayStats.value = TranslationUsageStats.fromJson(Map<String, dynamic>.from(statsData));
} else {
todayStats.value = TranslationUsageStats.empty(getCurrentUserId());
}
} catch (e) {
Logger.error('加载当日统计数据失败: $e');
todayStats.value = TranslationUsageStats.empty(getCurrentUserId());
}
}
/// 获取当前用户ID
String getCurrentUserId() {
try {
final userInfo = _storage.read('user_info');
if (userInfo != null && userInfo['user'] != null) {
return userInfo['user']['uid'] ?? 'anonymous';
}
} catch (e) {
Logger.warning('获取用户ID失败: $e');
}
return 'anonymous';
}
/// 记录翻译API调用
void recordTranslationApiCall({
required String sourceText,
required String targetText,
required String sourceLanguage,
required String targetLanguage,
required String mode,
}) {
try {
// 更新实时统计
realtimeStats.value.translationCount++;
realtimeStats.value.sourceTextCharacters += sourceText.length;
realtimeStats.value.ttsTextCharacters += targetText.length;
realtimeStats.value.currentLanguagePair = '$sourceLanguage-$targetLanguage';
realtimeStats.value.currentMode = mode;
realtimeStats.refresh();
// 更新当日统计 - 使用多维度统计
final languagePair = '$sourceLanguage→$targetLanguage';
// 更新语言对统计
final newLanguagePairStats = Map<String, LanguagePairStats>.from(todayStats.value.languagePairStats);
if (newLanguagePairStats.containsKey(languagePair)) {
final existing = newLanguagePairStats[languagePair]!;
newLanguagePairStats[languagePair] = LanguagePairStats(
languagePair: languagePair,
translationCount: existing.translationCount + 1,
sourceCharacters: existing.sourceCharacters + sourceText.length,
cost: existing.cost + _calculateTranslationCost(sourceText.length),
);
} else {
newLanguagePairStats[languagePair] = LanguagePairStats(
languagePair: languagePair,
translationCount: 1,
sourceCharacters: sourceText.length,
cost: _calculateTranslationCost(sourceText.length),
);
}
// 更新模式统计
final newModeStats = Map<String, TranslationModeStats>.from(todayStats.value.modeStats);
if (newModeStats.containsKey(mode)) {
final existing = newModeStats[mode]!;
newModeStats[mode] = existing.copyWith(
apiCalls: existing.apiCalls + 1,
sourceCharacters: existing.sourceCharacters + sourceText.length,
ttsCharacters: existing.ttsCharacters + targetText.length,
cost: existing.cost + _calculateTranslationCost(sourceText.length),
);
} else {
newModeStats[mode] = TranslationModeStats(
apiCalls: 1,
sourceCharacters: sourceText.length,
ttsCharacters: targetText.length,
cost: _calculateTranslationCost(sourceText.length),
);
}
todayStats.value = todayStats.value.copyWith(
translationApiCalls: todayStats.value.translationApiCalls + 1,
sourceTextCharacters: todayStats.value.sourceTextCharacters + sourceText.length,
ttsTextCharacters: todayStats.value.ttsTextCharacters + targetText.length,
languagePairStats: newLanguagePairStats,
modeStats: newModeStats,
estimatedCost: _calculateEstimatedCost(todayStats.value.copyWith(
translationApiCalls: todayStats.value.translationApiCalls + 1,
sourceTextCharacters: todayStats.value.sourceTextCharacters + sourceText.length,
ttsTextCharacters: todayStats.value.ttsTextCharacters + targetText.length,
)),
);
_saveTodayStats();
_checkCostThreshold();
Logger.info('记录翻译API调用: 源文本${sourceText.length}字符, 目标文本${targetText.length}字符');
} catch (e) {
Logger.error('记录翻译API调用失败: $e');
}
}
/// 记录ASR API调用 - 优化版(精确计费参数)
void recordAsrApiCall({
required int effectiveDurationSeconds, // 有效语音时长(VAD检测的实际语音)
required int totalDurationSeconds, // 总会话时长(包含静音)
required String language,
required String mode, // 翻译模式
String? audioFormat, // 音频格式(可选)
int? sampleRate, // 采样率(可选)
}) {
try {
// 更新实时统计(使用有效语音时长作为主要计费依据)
realtimeStats.value.effectiveAudioDuration += effectiveDurationSeconds;
realtimeStats.value.totalSessionDuration += totalDurationSeconds;
realtimeStats.refresh();
// 更新模式统计
final newModeStats = Map<String, TranslationModeStats>.from(todayStats.value.modeStats);
if (newModeStats.containsKey(mode)) {
final existing = newModeStats[mode]!;
newModeStats[mode] = existing.copyWith(
effectiveAudioDuration: existing.effectiveAudioDuration + effectiveDurationSeconds,
cost: existing.cost + _calculateAsrCost(effectiveDurationSeconds),
);
} else {
newModeStats[mode] = TranslationModeStats(
effectiveAudioDuration: effectiveDurationSeconds,
cost: _calculateAsrCost(effectiveDurationSeconds),
);
}
// 更新当日统计
todayStats.value = todayStats.value.copyWith(
asrApiCalls: todayStats.value.asrApiCalls + 1,
effectiveAudioDuration: todayStats.value.effectiveAudioDuration + effectiveDurationSeconds,
totalSessionDuration: todayStats.value.totalSessionDuration + totalDurationSeconds,
modeStats: newModeStats,
estimatedCost: _calculateEstimatedCost(todayStats.value.copyWith(
asrApiCalls: todayStats.value.asrApiCalls + 1,
effectiveAudioDuration: todayStats.value.effectiveAudioDuration + effectiveDurationSeconds,
)),
);
_saveTodayStats();
Logger.info('ASR统计: 有效语音${effectiveDurationSeconds}秒, 总时长${totalDurationSeconds}秒, 语言$language, 模式$mode');
} catch (e) {
Logger.error('记录ASR API调用失败: $e');
}
}
/// 记录TTS API调用
void recordTtsApiCall({
required String text,
required int durationSeconds,
required String language,
}) {
try {
// 更新实时统计
realtimeStats.value.totalSessionDuration += durationSeconds;
realtimeStats.refresh();
// 更新当日统计(TTS按字符数计费,不是按时长)
todayStats.value = todayStats.value.copyWith(
ttsApiCalls: todayStats.value.ttsApiCalls + 1,
ttsTextCharacters: todayStats.value.ttsTextCharacters + text.length,
totalSessionDuration: todayStats.value.totalSessionDuration + durationSeconds,
estimatedCost: _calculateEstimatedCost(todayStats.value.copyWith(
ttsApiCalls: todayStats.value.ttsApiCalls + 1,
ttsTextCharacters: todayStats.value.ttsTextCharacters + text.length,
)),
);
_saveTodayStats();
Logger.info('记录TTS API调用: 文本${text.length}字符, 时长${durationSeconds}秒');
} catch (e) {
Logger.error('记录TTS API调用失败: $e');
}
}
/// 记录AI摘要API调用
void recordAiSummaryApiCall() {
try {
// 更新当日统计
todayStats.value = todayStats.value.copyWith(
aiSummaryApiCalls: todayStats.value.aiSummaryApiCalls + 1,
estimatedCost: _calculateEstimatedCost(todayStats.value.copyWith(
aiSummaryApiCalls: todayStats.value.aiSummaryApiCalls + 1,
)),
);
_saveTodayStats();
Logger.info('记录AI摘要API调用');
} catch (e) {
Logger.error('记录AI摘要API调用失败: $e');
}
}
/// 开始新会话
void startNewSession() {
realtimeStats.value.reset();
// 更新当日会话数
todayStats.value = todayStats.value.copyWith(
sessionCount: todayStats.value.sessionCount + 1,
);
_saveTodayStats();
Logger.info('开始新的翻译会话');
}
/// 计算预估成本 - 优化版(基于精确计费参数)
double _calculateEstimatedCost(TranslationUsageStats stats) {
double cost = 0.0;
// 翻译成本:基于源文本字符数
cost += (stats.sourceTextCharacters / 1000) * (costConfig['translation_per_1k_chars'] ?? 0.05);
// ASR成本:基于有效音频时长
cost += (stats.effectiveAudioDuration / 60) * (costConfig['asr_per_minute'] ?? 0.02);
// TTS成本:基于TTS文本字符数
cost += (stats.ttsTextCharacters / 1000) * (costConfig['tts_per_1k_chars'] ?? 0.03);
// AI摘要成本(已废弃但保留兼容性)
cost += stats.aiSummaryApiCalls * (costConfig['ai_summary_per_request'] ?? 0.01);
return cost;
}
/// 计算翻译成本(单次调用)
double _calculateTranslationCost(int sourceCharacters) {
return (sourceCharacters / 1000) * (costConfig['translation_per_1k_chars'] ?? 0.05);
}
/// 计算ASR成本(单次调用)
double _calculateAsrCost(int durationSeconds) {
return (durationSeconds / 60) * (costConfig['asr_per_minute'] ?? 0.02);
}
/// 计算TTS成本(单次调用)
double _calculateTtsCost(int textCharacters) {
return (textCharacters / 1000) * (costConfig['tts_per_1k_chars'] ?? 0.03);
}
/// 保存当日统计数据
void _saveTodayStats() {
try {
final today = DateTime.now();
final todayKey = '${today.year}-${today.month.toString().padLeft(2, '0')}-${today.day.toString().padLeft(2, '0')}';
_storage.write('${_dailyStatsKey}_$todayKey', todayStats.value.toJson());
} catch (e) {
Logger.error('保存当日统计数据失败: $e');
}
}
/// 检查成本阈值
void _checkCostThreshold() {
final todayCost = todayStats.value.estimatedCost;
if (todayCost >= dailyCostThreshold.value) {
_showCostWarning('今日翻译成本已达到预警阈值:¥${todayCost.toStringAsFixed(2)}');
}
}
/// 显示成本预警
void _showCostWarning(String message) {
Get.snackbar(
'成本预警',
message,
snackPosition: SnackPosition.TOP,
backgroundColor: Get.theme.colorScheme.error.withOpacity(0.1),
colorText: Get.theme.colorScheme.error,
duration: const Duration(seconds: 5),
);
}
/// 获取历史统计数据
Future<List<TranslationUsageStats>> getHistoryStats({
required DateTime startDate,
required DateTime endDate,
}) async {
try {
final List<TranslationUsageStats> historyStats = [];
for (DateTime date = startDate;
date.isBefore(endDate) || date.isAtSameMomentAs(endDate);
date = date.add(const Duration(days: 1))) {
final dateKey = '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
final statsData = _storage.read('${_dailyStatsKey}_$dateKey');
if (statsData != null) {
historyStats.add(TranslationUsageStats.fromJson(Map<String, dynamic>.from(statsData)));
}
}
return historyStats;
} catch (e) {
Logger.error('获取历史统计数据失败: $e');
return [];
}
}
/// 清理过期数据(保留最近90天)
void cleanupOldData() {
try {
final cutoffDate = DateTime.now().subtract(const Duration(days: 90));
final allKeys = _storage.getKeys();
for (final key in allKeys) {
if (key.startsWith(_dailyStatsKey)) {
final dateStr = key.replaceFirst('${_dailyStatsKey}_', '');
try {
final date = DateTime.parse(dateStr);
if (date.isBefore(cutoffDate)) {
_storage.remove(key);
Logger.info('清理过期统计数据: $key');
}
} catch (e) {
// 忽略解析错误的键
}
}
}
} catch (e) {
Logger.error('清理过期数据失败: $e');
}
}
/// 更新成本配置
void updateCostConfig(Map<String, double> newConfig) {
costConfig.value = newConfig;
_saveCostConfig();
// 重新计算当日成本
todayStats.value = todayStats.value.copyWith(
estimatedCost: _calculateEstimatedCost(todayStats.value),
);
_saveTodayStats();
Logger.info('更新成本配置完成');
}
/// 更新预警阈值
void updateThresholds({
double? dailyThreshold,
double? monthlyThreshold,
}) {
if (dailyThreshold != null) {
dailyCostThreshold.value = dailyThreshold;
}
if (monthlyThreshold != null) {
monthlyCostThreshold.value = monthlyThreshold;
}
// 保存阈值配置
_storage.write('daily_cost_threshold', dailyCostThreshold.value);
_storage.write('monthly_cost_threshold', monthlyCostThreshold.value);
Logger.info('更新预警阈值完成');
}
/// 获取月度统计汇总
Future<TranslationUsageStats> getMonthlyStats(int year, int month) async {
try {
final startDate = DateTime(year, month, 1);
final endDate = DateTime(year, month + 1, 0); // 月末
final dailyStats = await getHistoryStats(
startDate: startDate,
endDate: endDate,
);
if (dailyStats.isEmpty) {
return TranslationUsageStats.empty(getCurrentUserId());
}
// 汇总月度数据
int totalTranslationCalls = 0;
int totalAsrCalls = 0;
int totalTtsCalls = 0;
int totalAiSummaryCalls = 0;
int totalSourceChars = 0;
int totalTargetChars = 0;
int totalRecordingDuration = 0;
int totalTtsPlayDuration = 0;
int totalSessions = 0;
double totalCost = 0.0;
final Map<String, int> combinedLanguagePairUsage = {};
final Map<String, int> combinedModeUsage = {};
for (final stats in dailyStats) {
totalTranslationCalls += stats.translationApiCalls;
totalAsrCalls += stats.asrApiCalls;
totalTtsCalls += stats.ttsApiCalls;
totalAiSummaryCalls += stats.aiSummaryApiCalls;
totalSourceChars += stats.sourceCharacterCount;
totalTargetChars += stats.targetCharacterCount;
totalRecordingDuration += stats.audioRecordingDuration;
totalTtsPlayDuration += stats.ttsPlaybackDuration;
totalSessions += stats.sessionCount;
totalCost += stats.estimatedCost;
// 合并语言对使用统计
stats.languagePairUsage.forEach((key, value) {
combinedLanguagePairUsage[key] = (combinedLanguagePairUsage[key] ?? 0) + value;
});
// 合并模式使用统计
stats.modeUsage.forEach((key, value) {
combinedModeUsage[key] = (combinedModeUsage[key] ?? 0) + value;
});
}
return TranslationUsageStats(
id: 'monthly_${year}_$month',
date: startDate,
userId: getCurrentUserId(),
timeGranularity: 'month',
translationApiCalls: totalTranslationCalls,
asrApiCalls: totalAsrCalls,
ttsApiCalls: totalTtsCalls,
aiSummaryApiCalls: totalAiSummaryCalls,
sourceTextCharacters: totalSourceChars,
effectiveAudioDuration: totalRecordingDuration,
ttsTextCharacters: totalTargetChars,
totalSessionDuration: totalTtsPlayDuration,
sessionCount: totalSessions,
estimatedCost: totalCost,
);
} catch (e) {
Logger.error('获取月度统计失败: $e');
return TranslationUsageStats.empty(getCurrentUserId());
}
}
}

2
lib/data/services/user_portrait.dart

@ -1,4 +1,4 @@
import 'package:deep_voice/core/utils/logger.dart';
import '/core/utils/logger.dart';
import 'package:get/get.dart';
import 'package:user_portrait/user_portrait.dart';

38
lib/data/services/version_update_service.dart

@ -7,6 +7,7 @@ import 'package:logger/logger.dart';
import 'package:device_info_plus/device_info_plus.dart';
import '../models/channel_app_model.dart';
import 'network/api.dart';
import 'dart:math' as math;
/// 版本更新服务类
class VersionUpdateService extends GetxService {
@ -128,7 +129,7 @@ class VersionUpdateService extends GetxService {
final response =
await Api.getChannelApp({'channel': platformInfo.channel.value});
print('获取渠道包信息:$response');
_logger.d('获取渠道包信息:$response');
if (response == null || !response.containsKey('app')) {
return VersionUpdateInfo(
@ -142,6 +143,7 @@ class VersionUpdateService extends GetxService {
//final channelAppResponse = ChannelAppResponse.fromJson(response);
// 直接从response中获取app信息
final appInfo = AppInfo.fromJson(response['app']);
_logger.d('获取到的最新版本信息: ${appInfo.toJson()}');
// 设置游客登录显示状态
isShowTourists.value = appInfo.tourists;
@ -170,8 +172,8 @@ class VersionUpdateService extends GetxService {
return VersionUpdateInfo(
hasUpdate: hasUpdate,
currentVersion: currentVersion,
latestVersion: appInfo.version,
currentVersion: currentVersion.split('+')[0],
latestVersion: appInfo.version.split('+')[0],
appInfo: appInfo,
updateType: updateType,
);
@ -204,7 +206,14 @@ class VersionUpdateService extends GetxService {
// 比较主版本号(第一位)
if (currentVersions.isNotEmpty && latestVersions.isNotEmpty) {
return currentVersions[0] != latestVersions[0];
if (currentVersions[0] != latestVersions[0]) {
return true;
}
}
// 检查是否有两个或更多的 + 号,如果有则强制更新
if (latest.split('+').length > 2) {
return true;
}
return false;
@ -218,24 +227,25 @@ class VersionUpdateService extends GetxService {
/// 返回true表示需要更新(远程版本更高)
bool _compareVersions(String current, String latest) {
// 拆分版本号与构建号
final currentParts = current.split('+');
final latestParts = latest.split('+');
final currentParts = current.split('+')[0];
final latestParts = latest.split('+')[0];
// 解析版本号 (major.minor.patch)
final currentVersions = currentParts[0].split('.').map(int.parse).toList();
final latestVersions = latestParts[0].split('.').map(int.parse).toList();
final currentVersions = currentParts.split('.').map(int.parse).toList();
final latestVersions = latestParts.split('.').map(int.parse).toList();
// 使用较小的长度进行比较,避免索引越界
final minLength = math.min(currentVersions.length, latestVersions.length);
// 逐级比较版本号
for (var i = 0; i < currentVersions.length; i++) {
for (var i = 0; i < minLength; i++) {
if (latestVersions[i] > currentVersions[i]) return true;
if (latestVersions[i] < currentVersions[i]) return false;
}
// 版本号相同则比较构建号
if (latestParts.length > 1 && currentParts.length > 1) {
return int.parse(latestParts[1]) > int.parse(currentParts[1]);
}
return false;
// 如果前面的版本号都相同,比较长度
// 更长的版本号被认为是更新的版本(如 1.0.1 > 1.0)
return latestVersions.length > currentVersions.length;
}
/// 解析版本号字符串为数字数组

65
lib/modules/agent/controllers/agent_controller.dart

@ -152,6 +152,9 @@ class AgentController extends GetxController {
_initMusicStatus(); //初始化音乐状态
_setupMediaSessionCallbacks(); //设置媒体会话回调
_setupTextInputListener(); //设置文本输入监听器
_configureSpeechRecognitionLanguages(); //配置语音识别语言
startVoiceInput(); //开启语音输入
}
@ -465,7 +468,7 @@ class AgentController extends GetxController {
case AgentServiceEventType.recognitionStarted:
isListening.value = true;
isTextInputMode.value = false;
print("订阅代理服务事件识别开始");
Logger.i(TAG, '订阅代理服务事件识别开始');
// isTextInputMode.refresh();
// 只标记识别开始,不添加消息
Logger.i(TAG, '识别开始');
@ -1010,10 +1013,54 @@ class AgentController extends GetxController {
}
}
//根据APP语言配置语音识别语言
Future<void> _configureSpeechRecognitionLanguages() async {
try {
// 1. 使用 GetX 获取 APP 当前语言环境 (Locale)
final appLocale = Get.locale;
List<String> languagesToSupport = [];
if (appLocale != null) {
final lang = appLocale.languageCode; // e.g., 'en'
final country = appLocale.countryCode; // e.g., 'US'
// 2. 构造 Azure SDK 需要的格式 (e.g., 'en-US')
if (country != null && country.isNotEmpty) {
languagesToSupport.add('$lang-$country');
} else {
languagesToSupport.add(lang); // fallback only language code
}
}
// 3. 添加 en-US 作为备选
if (!languagesToSupport.contains('en-US')) {
languagesToSupport.add('en-US');
}
// 4. 最终兜底
if (languagesToSupport.isEmpty) {
languagesToSupport.add('en-US');
}
// 5. 调用原生设置
Logger.i(TAG, '支持的语言: ${languagesToSupport.join(', ')}');
AgentService.setSupportedLanguages(languagesToSupport).then((success) {
if (success) {
Logger.i(TAG, 'asr的语言已根据 APP 设置语言: ${languagesToSupport.join(', ')}');
} else {
Logger.w(TAG, '根据 APP 设置asr语言失败');
}
});
} catch (e) {
Logger.e(TAG, "获取 APP 语言并设置失败: $e");
AgentService.setSupportedLanguages(['zh-CN', 'en-US']);
}
}
// 开始语音输入
Future<void> startVoiceInput() async {
if (isListening.value) return;
print('开始语音输入'); // 开始语音输入
Logger.i(TAG, '开始语音输入'); // 开始语音输入
final hasPermission = await PermissionUtil.instance.requestPermission(
// context: Get.context!,
permissionType: Permission.microphone,
@ -1309,7 +1356,7 @@ class AgentController extends GetxController {
// 打电话模式不使用按住说话模式,而是持续语音输入
//isPushToTalkMode.value = false;
// 开始语音识别
print("开始打电话模式,开始语音输入");
Logger.i(TAG, '开始打电话模式,开始语音输入');
await AgentService.startConversation();
//}
}
@ -1446,11 +1493,12 @@ class AgentController extends GetxController {
var status = await _musiceManager.getStatus();
isPlaying.value = status['isPlaying'] ?? false;
currentId.value = status['id'] as int;
Logger.d('AgentController',
'音乐状态更新: playing=${isPlaying.value}, url=${currentId.value}');
print("音乐状态更新: playing=${isPlaying.value}, url=${currentId.value}");
Logger.d(
TAG, '音乐状态更新: playing=${isPlaying.value}, url=${currentId.value}');
Logger.d(
TAG, '音乐状态更新: playing=${isPlaying.value}, url=${currentId.value}');
} catch (e) {
Logger.e('AgentController', '获取音乐状态失败: $e');
Logger.e(TAG, '获取音乐状态失败: $e');
}
}
@ -1487,7 +1535,8 @@ class AgentController extends GetxController {
var response = await Api.getMusicUrl({
"ids": [id]
});
print('getMusicUrl 响应数据: ${response.toString()}'); // 调试输出原始数据
Logger.d(
TAG, 'getMusicUrl 响应数据: ${response.toString()}'); // 调试输出原始数据
final urldata = response['urls'][0] as Map<String, dynamic>;
song["url"] = urldata['url'] ?? '';
// 修复:确保currentId接收int类型

357
lib/modules/agent/views/agent_view.dart

@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/agent_controller.dart';
@ -61,40 +62,40 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
backgroundColor: backgroundColor,
leading: IconButton(
icon: Icon(Icons.arrow_back_ios,
color: isDarkMode ? Colors.white : Colors.black87, size: 20),
color: isDarkMode ? Colors.white : Colors.black87, size: 20.sp),
onPressed: () => Get.back(),
),
titleSpacing: 0,
title: Row(
children: [
Container(
width: 40,
height: 40,
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
color: primaryColor,
borderRadius: BorderRadius.circular(20),
borderRadius: BorderRadius.circular(20.w),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 3,
blurRadius: 3.w,
offset: const Offset(0, 1),
),
],
),
child: const Icon(
child: Icon(
Icons.smart_toy_outlined,
color: Colors.white,
size: 22,
size: 22.sp,
),
),
const SizedBox(width: 12),
SizedBox(width: 12.w),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'XiaoYan'.tr,
style: TextStyle(
fontSize: 16,
fontSize: 16.sp,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black87,
),
@ -102,18 +103,18 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
Row(
children: [
Container(
width: 6,
height: 6,
width: 6.w,
height: 6.w,
decoration: const BoxDecoration(
color: Colors.green,
shape: BoxShape.circle,
),
),
const SizedBox(width: 4),
SizedBox(width: 4.w),
Text(
'online'.tr,
style: TextStyle(
fontSize: 12,
fontSize: 12.sp,
color: Colors.green,
fontWeight: FontWeight.w500,
),
@ -131,6 +132,7 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
controller.isTtsEnabled.value
? Icons.volume_up
: Icons.volume_off,
size: 24.sp,
color: controller.isTtsEnabled.value
? primaryColor
: isDarkMode
@ -142,9 +144,9 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
// 更多选项
Container(
width: 34,
height: 34,
margin: const EdgeInsets.only(right: 8),
width: 34.w,
height: 34.w,
margin: EdgeInsets.only(right: 8.w),
decoration: BoxDecoration(
color:
isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100],
@ -153,25 +155,28 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
if (!isDarkMode)
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
spreadRadius: 0.5,
blurRadius: 2.w,
spreadRadius: 0.5.w,
),
],
),
child: PopupMenuButton(
icon: Icon(Icons.more_vert,
color: isDarkMode ? Colors.white : Colors.black87, size: 20),
color: isDarkMode ? Colors.white : Colors.black87,
size: 20.sp),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(12.w),
),
color: isDarkMode ? Colors.grey[800] : Colors.white,
itemBuilder: (context) => [
PopupMenuItem(
child: Row(
children: [
Icon(Icons.delete_outline, color: primaryColor, size: 20),
const SizedBox(width: 8),
Text('clearChatHistory'.tr, style: TextStyle(fontSize: 14)),
Icon(Icons.delete_outline,
color: primaryColor, size: 20.sp),
SizedBox(width: 8.w),
Text('clearChatHistory'.tr,
style: TextStyle(fontSize: 14.sp)),
],
),
onTap: () => Future.delayed(
@ -198,8 +203,8 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
controller: controller.scrollController,
shrinkWrap: true,
reverse: true,
padding: const EdgeInsets.symmetric(
vertical: 16, horizontal: 16),
padding: EdgeInsets.symmetric(
vertical: 16.h, horizontal: 16.w),
itemCount: controller.messages.length +
(controller.hasMoreMessages.value ? 1 : 0),
itemBuilder: (context, index) {
@ -212,7 +217,7 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
final message = controller
.messages[controller.messages.length - 1 - index];
return Padding(
padding: const EdgeInsets.only(bottom: 8),
padding: EdgeInsets.only(bottom: 8.h),
child: GestureDetector(
onLongPressStart:
(LongPressStartDetails details) {
@ -237,21 +242,20 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
// 输入区域
Container(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h),
decoration: BoxDecoration(
color: backgroundColor,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.06),
blurRadius: 8,
blurRadius: 8.w,
offset: const Offset(0, -1),
),
],
border: Border(
top: BorderSide(
color: Colors.grey.withOpacity(0.15),
width: 1,
width: 1.w,
),
),
),
@ -269,29 +273,29 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(12.w),
child: Image.file(
File(controller.selectedImagePath.value!),
height: 150,
height: 150.h,
width: double.infinity,
fit: BoxFit.cover,
),
),
Positioned(
top: 8,
right: 8,
top: 8.h,
right: 8.w,
child: GestureDetector(
onTap: () => controller.clearImageInput(),
child: Container(
padding: const EdgeInsets.all(4),
padding: EdgeInsets.all(4.w),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.6),
shape: BoxShape.circle,
),
child: const Icon(
child: Icon(
Icons.close,
color: Colors.white,
size: 16,
size: 16.sp,
),
),
),
@ -300,34 +304,34 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
),
// 预设提示选项
const SizedBox(height: 12),
SizedBox(height: 12.h),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
_buildPromptChip(
context: context,
label: 'summarizeImageContent'.tr,//总结图片内容
label: 'summarizeImageContent'.tr, //总结图片内容
onTap: () => controller.summarizeImage(),
),
const SizedBox(width: 8),
SizedBox(width: 8.w),
_buildPromptChip(
context: context,
label: 'extractImageText'.tr,//抽取图像文字
label: 'extractImageText'.tr, //抽取图像文字
onTap: () =>
controller.extractTextFromImage(),
),
const SizedBox(width: 8),
SizedBox(width: 8.w),
_buildPromptChip(
context: context,
label: 'translateImageText'.tr,//翻译图像文字
label: 'translateImageText'.tr, //翻译图像文字
onTap: () =>
controller.translateImageText(),
),
],
),
),
const SizedBox(height: 12),
SizedBox(height: 12.h),
],
);
} else {
@ -353,8 +357,8 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
controller.toggleInputMode();
},
child: Container(
width: 44,
height: 44,
width: 44.w,
height: 44.w,
decoration: BoxDecoration(
color: isDarkMode
? Colors.grey[800]
@ -368,13 +372,13 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
color: isDarkMode
? Colors.grey[300]
: Colors.grey[700],
size: 22,
size: 22.sp,
),
),
);
}),
const SizedBox(width: 8),
SizedBox(width: 8.w),
// 文本输入框或语音波形图
Expanded(
@ -385,23 +389,23 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
if (controller.isTextInputMode.value) {
// 文本输入模式 - 限制最大高度并只向上扩展
return Container(
constraints: const BoxConstraints(
minHeight: 44, // 最小高度
maxHeight: 120, // 最大高度限制,防止挤压按钮
constraints: BoxConstraints(
minHeight: 44.h,
maxHeight: 120.h,
),
padding: const EdgeInsets.symmetric(
horizontal: 16),
padding: EdgeInsets.symmetric(
horizontal: 16.w),
decoration: BoxDecoration(
color: isDarkMode
? Colors.grey[800]
: Colors.grey[200],
borderRadius: BorderRadius.circular(22),
borderRadius:
BorderRadius.circular(22.w),
border: Border.all(
color: isDarkMode
? Colors.grey[
800]! // 修改: 从 Colors.grey[700] 改为 Colors.grey[800]
? Colors.grey[800]!
: Colors.grey.withOpacity(0.1),
width: 1.0,
width: 1.w,
),
),
child: Row(
@ -435,7 +439,7 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
}
},
style: TextStyle(
fontSize: 15,
fontSize: 15.sp,
color: isDarkMode
? Colors.white
: Colors.grey[800],
@ -448,7 +452,7 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
.tr, // 输入消息...
hintStyle: TextStyle(
color: Colors.grey[500],
fontSize: 15,
fontSize: 15.sp,
),
filled: false,
border: InputBorder.none,
@ -458,14 +462,15 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
color: isDarkMode
? Colors.grey[800]!
: Colors.grey[200]!,
width: 1.0,
width: 1.w,
),
),
contentPadding:
EdgeInsets.symmetric(
vertical: isImageActive
? 12
: 8), // 调整垂直内边距
vertical:
isImageActive
? 12.h
: 8.h),
),
onSubmitted: (_) =>
controller.sendMessage(),
@ -525,9 +530,9 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
controller.isListening.value;
return Container(
height: 50,
padding: const EdgeInsets.symmetric(
horizontal: 16),
height: 52.h,
padding: EdgeInsets.symmetric(
horizontal: 16.w),
decoration: BoxDecoration(
color: isPushToTalkActive
? (isDarkMode
@ -537,7 +542,7 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
? Colors.grey[800]
: Colors.grey[200]),
borderRadius:
BorderRadius.circular(22),
BorderRadius.circular(22.w),
border: Border.all(
color: isPushToTalkActive
? Colors.blue
@ -546,8 +551,8 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
: Colors.grey
.withOpacity(0.1)),
width: isPushToTalkActive
? 2.0
: 1.0,
? 2.w
: 1.w,
),
),
child: Row(
@ -572,7 +577,7 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
isPushToTalkActive
? Icons.mic
: Icons.mic_none,
size: 16,
size: 16.sp,
color: isPushToTalkActive
? Colors.blue
: Colors.grey[500],
@ -607,7 +612,7 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
}),
),
const SizedBox(width: 8),
SizedBox(width: 8.w),
// 动态按钮区域:展开按钮 + 发送按钮 或 图片选择按钮
Obx(() {
@ -627,7 +632,7 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
if (shouldShowExpand && hasText)
Padding(
padding:
const EdgeInsets.only(bottom: 40),
EdgeInsets.only(bottom: 40.h),
child: GestureDetector(
onTap: () {
// 打开底部弹出编辑模式
@ -646,21 +651,21 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
});
},
child: Container(
width: 32,
height: 32,
width: 32.w,
height: 32.w,
decoration: BoxDecoration(
color: isDarkMode
? Colors.grey[700]
: Colors.grey[300],
borderRadius:
BorderRadius.circular(16),
BorderRadius.circular(16.w),
),
child: Icon(
Icons.expand_less,
color: isDarkMode
? Colors.grey[300]
: Colors.grey[700],
size: 20,
size: 20.sp,
),
),
),
@ -669,17 +674,17 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
GestureDetector(
onTap: () => controller.sendMessage(),
child: Container(
width: 44,
height: 44,
width: 44.w,
height: 44.w,
decoration: BoxDecoration(
color: primaryColor,
borderRadius:
BorderRadius.circular(22),
BorderRadius.circular(22.w),
),
child: const Icon(
child: Icon(
Icons.send,
color: Colors.white,
size: 22,
size: 22.sp,
),
),
),
@ -695,21 +700,21 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
controller.togglePlusOptions();
},
child: Container(
width: 44,
height: 44,
width: 44.w,
height: 44.w,
decoration: BoxDecoration(
color: isDarkMode
? Colors.grey[800]
: Colors.grey[200],
borderRadius:
BorderRadius.circular(22),
BorderRadius.circular(22.w),
),
child: Icon(
isExpanded ? Icons.close : Icons.add,
color: isDarkMode
? Colors.grey[300]
: Colors.grey[700],
size: 22,
size: 22.sp,
),
),
);
@ -727,62 +732,77 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
height: isExpanded ? 120.h : 0, // 调整高度以适应按钮尺寸
height: isExpanded ? 100.h : 0, // 调整高度以适应按钮尺寸
child: isExpanded
? Container(
margin: EdgeInsets.only(top: 8.h),
padding: EdgeInsets.symmetric(
horizontal: 16.w), // 添加左右边距
child: Row(
mainAxisAlignment: MainAxisAlignment
.spaceEvenly, // 改为均匀分布
children: [
// 相机按钮
_buildPlusOptionButton(
context: context,
icon: Icons.camera_alt,
label: 'takePhoto'.tr, // 拍照
onTap: () async {
//controller.collapsePlusOptions();
await controller.takePhoto();
},
isDarkMode: isDarkMode,
),
// 相册按钮
_buildPlusOptionButton(
context: context,
icon: Icons.photo_library,
label: 'photoAlbum'.tr, // 相册
onTap: () async {
//controller.collapsePlusOptions();
await controller.pickImage();
},
isDarkMode: isDarkMode,
),
// 文件按钮(预留位置)
// _buildPlusOptionButton(
// context: context,
// icon: Icons.folder,
// label: '文件',
// onTap: () {
// //controller.collapsePlusOptions();
// // TODO: 实现文件选择功能
// },
// isDarkMode: isDarkMode,
// ),
// 通话按钮
_buildPlusOptionButton(
context: context,
icon: Icons.phone,
label: 'makeCall'.tr, // 打电话
onTap: () {
// 启动打电话模式
controller.startPhoneCall();
},
isDarkMode: isDarkMode,
? LayoutBuilder(
// 添加LayoutBuilder获取实际约束
builder: (context, constraints) {
return SingleChildScrollView(
// 确保内容可滚动
scrollDirection:
Axis.horizontal, // 水平滚动
child: Container(
margin: EdgeInsets.only(top: 16.h),
padding: EdgeInsets.symmetric(
horizontal: 16.w),
// 根据可用宽度调整容器宽度
width: max(constraints.maxWidth,
300.w), // 最小300宽
child: Wrap(
// 使用Wrap替代Row,自动换行
alignment: WrapAlignment.center,
spacing: 26.w, // 按钮水平间距
runSpacing: 8.h, // 按钮垂直间距
children: [
// 相机按钮
_buildPlusOptionButton(
context: context,
icon: Icons.camera_alt,
label: 'takePhoto'.tr, // 拍照
onTap: () async {
//controller.collapsePlusOptions();
await controller.takePhoto();
},
isDarkMode: isDarkMode,
),
// 相册按钮
_buildPlusOptionButton(
context: context,
icon: Icons.photo_library,
label: 'photoAlbum'.tr, // 相册
onTap: () async {
//controller.collapsePlusOptions();
await controller.pickImage();
},
isDarkMode: isDarkMode,
),
// 文件按钮(预留位置)
// _buildPlusOptionButton(
// context: context,
// icon: Icons.folder,
// label: '文件',
// onTap: () {
// //controller.collapsePlusOptions();
// // TODO: 实现文件选择功能
// },
// isDarkMode: isDarkMode,
// ),
// 通话按钮
_buildPlusOptionButton(
context: context,
icon: Icons.phone,
label: 'makeCall'.tr, // 打电话
onTap: () {
// 启动打电话模式
controller.startPhoneCall();
},
isDarkMode: isDarkMode,
),
],
),
),
],
),
);
},
)
: const SizedBox.shrink(),
);
@ -813,25 +833,24 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
required String label,
required VoidCallback onTap,
}) {
final theme = Theme.of(context);
final primaryColor = theme.primaryColor;
final primaryColor = Theme.of(context).primaryColor;
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
decoration: BoxDecoration(
color: primaryColor.withOpacity(0.08),
borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(16.w),
border: Border.all(
color: primaryColor.withOpacity(0.2),
width: 1,
width: 1.w,
),
),
child: Text(
label,
style: TextStyle(
fontSize: 13,
fontSize: 13.sp,
color: primaryColor,
fontWeight: FontWeight.w500,
),
@ -865,10 +884,10 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
height: 50.w, // 调整为44.w以适应布局
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.grey[100],
borderRadius: BorderRadius.circular(12.r), // 调整为10.r
borderRadius: BorderRadius.circular(12.w),
border: Border.all(
color: isDarkMode ? Colors.grey[700]! : Colors.grey[300]!,
width: 1,
width: 1.w,
),
),
child: Icon(
@ -1085,8 +1104,8 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
BoxShadow(
color: (isActive ? Colors.red : Colors.green)
.withOpacity(0.3),
blurRadius: 12.r,
spreadRadius: 2.r,
blurRadius: 12.w,
spreadRadius: 2.w,
),
],
),
@ -1148,23 +1167,23 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
if (controller.isLoadingMore.value) {
// 正在加载状态
return Container(
padding: const EdgeInsets.symmetric(vertical: 16),
padding: EdgeInsets.symmetric(vertical: 16.h),
child: Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
width: 16,
height: 16,
SizedBox(
width: 16.w,
height: 16.h,
child: CircularProgressIndicator(
strokeWidth: 2,
strokeWidth: 2.w,
),
),
const SizedBox(width: 8),
SizedBox(width: 8.w),
Text(
'loadMoreMessages'.tr, // 加载更多消息...
style: const TextStyle(
fontSize: 14,
'loadMoreMessages'.tr,
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey,
),
),
@ -1175,23 +1194,23 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
} else if (controller.isLoadingMoreError.value) {
// 加载失败状态
return Container(
padding: const EdgeInsets.symmetric(vertical: 16),
padding: EdgeInsets.symmetric(vertical: 16.h),
child: Center(
child: GestureDetector(
onTap: () => controller.loadMoreMessages(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icon(
Icons.refresh,
size: 16,
size: 16.sp,
color: Colors.grey,
),
const SizedBox(width: 8),
SizedBox(width: 8.w),
Text(
'loadFailedClickRetry'.tr, // 加载失败,点击重试
style: const TextStyle(
fontSize: 14,
'loadFailedClickRetry'.tr,
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey,
),
),
@ -1203,12 +1222,12 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
} else {
// 默认状态(有更多消息但未加载)
return Container(
padding: const EdgeInsets.symmetric(vertical: 8),
padding: EdgeInsets.symmetric(vertical: 8.h),
child: Center(
child: Text(
'swipeUpForMore'.tr, // 向上滑动查看更多
style: const TextStyle(
fontSize: 12,
'swipeUpForMore'.tr,
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey,
),
),

77
lib/modules/agent/views/message_bubble.dart

@ -33,8 +33,6 @@ class MessageBubble extends GetView<AgentController> {
@override
Widget build(BuildContext context) {
final timeFormat = DateFormat('HH:mm');
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
@ -91,7 +89,7 @@ class MessageBubble extends GetView<AgentController> {
children: [
// 时间戳(左对齐或右对齐)
Text(
timeFormat.format(timestamp),
_formatTimestamp(timestamp),
style: TextStyle(
fontSize: 10,
color: Theme.of(context).brightness == Brightness.dark
@ -105,7 +103,7 @@ class MessageBubble extends GetView<AgentController> {
Padding(
padding: const EdgeInsets.only(left: 8),
child: Text(
"generatedByAi".tr,// 由AI生成
"generatedByAi".tr, // 由AI生成
style: TextStyle(
fontSize: 8,
color: Colors.blue[600],
@ -418,6 +416,77 @@ class MessageBubble extends GetView<AgentController> {
Widget _buildDot(int milliseconds) {
return _AnimatedDot(delayMilliseconds: milliseconds);
}
/// 智能格式化时间戳
String _formatTimestamp(DateTime messageTime) {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final yesterday = today.subtract(const Duration(days: 1));
final messageDate =
DateTime(messageTime.year, messageTime.month, messageTime.day);
// 计算时间差
final difference = now.difference(messageTime);
// 1分钟内显示"刚刚"
if (difference.inMinutes < 1) {
return 'justNow'.tr; // 刚刚
}
// 1小时内显示"X分钟前"
if (difference.inHours < 1) {
return 'minutesAgo'
.trParams({'minutes': difference.inMinutes.toString()}); // X分钟前
}
// 今天的消息显示时间
if (messageDate == today) {
return DateFormat('HH:mm').format(messageTime);
}
// 昨天的消息显示"昨天 HH:mm"
if (messageDate == yesterday) {
return 'yesterday'.tr +
' ' +
DateFormat('HH:mm').format(messageTime); // 昨天 HH:mm
}
// 本周内的消息显示"星期X HH:mm"
if (difference.inDays < 7) {
final weekday = _getWeekdayName(messageTime.weekday);
return weekday + ' ' + DateFormat('HH:mm').format(messageTime);
}
// 今年的消息显示"MM-dd HH:mm"
if (messageTime.year == now.year) {
return DateFormat('MM-dd HH:mm').format(messageTime);
}
// 其他情况显示完整日期"yyyy-MM-dd HH:mm"
return DateFormat('yyyy-MM-dd HH:mm').format(messageTime);
}
/// 获取星期名称
String _getWeekdayName(int weekday) {
switch (weekday) {
case 1:
return 'monday'.tr; // 星期一
case 2:
return 'tuesday'.tr; // 星期二
case 3:
return 'wednesday'.tr; // 星期三
case 4:
return 'thursday'.tr; // 星期四
case 5:
return 'friday'.tr; // 星期五
case 6:
return 'saturday'.tr; // 星期六
case 7:
return 'sunday'.tr; // 星期日
default:
return '';
}
}
}
/// 动画点组件

324
lib/modules/agent/views/navigation_card.dart

@ -221,89 +221,108 @@ Widget navigationCard(
// 底部:目的地信息和操作按钮
Container(
padding: EdgeInsets.fromLTRB(20.w, 8.h, 20.w, 16.h),
child: Stack(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 目的地和距离时间信息
Padding(
padding: EdgeInsets.only(right: 70.w), // 为右下角按钮留出空间
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 路径显示:当前位置 → 目的地
Row(
children: [
// 起点图标
Icon(
Icons.my_location,
size: 16.sp,
color: Theme.of(context).primaryColor,
),
// SizedBox(width: 6.w),
// // 当前位置文本
// Text(
// 'currentLocation'.tr, // 当前位置
// style: TextStyle(
// fontSize: 14.sp,
// fontWeight: FontWeight.w500,
// color: isDarkMode
// ? Colors.white70
// : Colors.black54,
// ),
// ),
SizedBox(width: 8.w),
// 箭头图标
Icon(
Icons.arrow_forward,
size: 16.sp,
color:
isDarkMode ? Colors.white54 : Colors.black45,
),
SizedBox(width: 8.w),
// 目的地图标
Icon(
Icons.location_on,
size: 16.sp,
color: Colors.red,
),
SizedBox(width: 6.w),
// 目的地名称
Expanded(
child: Text(
destination,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: isDarkMode
? Colors.white
: Colors.black87,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
// 第一层:起点 → 目的地信息(独占整个宽度)
Row(
crossAxisAlignment: CrossAxisAlignment.center, // 确保垂直居中对齐
children: [
// 起点图标 - 统一图标容器宽度,确保对齐
SizedBox(
width: 16.sp, // 固定图标容器宽度
child: Icon(
Icons.my_location,
size: 15.sp, // 统一图标大小
color: Theme.of(context).primaryColor,
),
),
SizedBox(width: 6.w), // 统一间距
// 当前位置文本
Text(
'currentLocation'.tr, // 当前位置
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: isDarkMode
? Colors.white70
: Colors.black54,
height: 1.3, // 设置行高,提升多行文本可读性
),
SizedBox(height: 4.h),
// 距离和时间信息
Column(
softWrap: true, // 启用软换行
overflow: TextOverflow.visible, // 确保文本完全显示
),
SizedBox(width: 8.w),
// 箭头图标
Icon(
Icons.arrow_forward,
size: 15.sp, // 统一图标大小
color: isDarkMode ? Colors.white54 : Colors.black45,
),
SizedBox(width: 8.w),
// 目的地图标
Icon(
Icons.location_on,
size: 15.sp, // 统一图标大小
color: Colors.red,
),
SizedBox(width: 6.w), // 统一间距
// 目的地名称 - 使用Expanded确保自动换行
Expanded(
child: Text(
destination,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: isDarkMode
? Colors.white
: Colors.black87,
height: 1.2, // 设置行高,提升多行文本可读性
),
softWrap: true, // 启用软换行
overflow: TextOverflow.visible, // 确保文本完全显示
),
),
],
),
SizedBox(height: 4.h), // 第一层和第二层之间的间距
// 第二层:距离时间信息(左侧)+ 导航按钮(右侧)
Row(
crossAxisAlignment: CrossAxisAlignment.end, // 底部对齐
children: [
// 左侧:距离和时间信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 距离信息
Row(
crossAxisAlignment: CrossAxisAlignment.center, // 确保垂直居中对齐
children: [
Icon(
Icons.straighten,
size: 14.sp,
color: Theme.of(context).primaryColor,
),
SizedBox(width: 4.w),
Text(
formatDistance(
modes[currentIndex]["distance"]),
style: TextStyle(
fontSize: 14.sp,
// 距离图标 - 统一图标容器宽度,确保与第一层对齐
SizedBox(
width: 16.sp, // 与第一层图标容器宽度一致
child: Icon(
Icons.straighten,
size: 15.sp, // 统一图标大小
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.w500,
),
),
SizedBox(width: 6.w), // 与第一层间距一致
Flexible(
child: Text(
formatDistance(
modes[currentIndex]["distance"]),
style: TextStyle(
fontSize: 14.sp,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.w500,
height: 1.3, // 设置行高,提升多行文本可读性
),
softWrap: true, // 启用软换行
overflow: TextOverflow.visible, // 确保文本完全显示
),
),
],
@ -311,95 +330,106 @@ Widget navigationCard(
SizedBox(height: 4.h),
// 时间信息
Row(
crossAxisAlignment: CrossAxisAlignment.center, // 确保垂直居中对齐
children: [
Icon(
Icons.access_time,
size: 14.sp,
color: Theme.of(context).primaryColor,
// 时间图标 - 统一图标容器宽度,确保与第一层对齐
SizedBox(
width: 16.sp, // 与第一层图标容器宽度一致
child: Icon(
Icons.access_time,
size: 15.sp, // 统一图标大小
color: Theme.of(context).primaryColor,
),
),
SizedBox(width: 4.w),
Text(
'${'approximate'.tr}${formatDuration(modes[currentIndex]["duration"])}', // 约
SizedBox(width: 6.w), // 与第一层间距一致
Flexible(
child: Text(
'${'approximate'.tr}${formatDuration(modes[currentIndex]["duration"])}', // 约
style: TextStyle(
fontSize: 14.sp,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.w500,
height: 1.3, // 设置行高,提升多行文本可读性
),
softWrap: true, // 启用软换行
overflow: TextOverflow.visible, // 确保文本完全显示
),
),
],
),
// 导航状态信息
if (navigation != null) ...[
SizedBox(height: 6.h),
Container(
padding: EdgeInsets.symmetric(
horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color: Theme.of(context)
.primaryColor
.withOpacity(0.1),
borderRadius: BorderRadius.circular(6.r),
),
child: Text(
"${navigation["distanceText"]}${'enterRoad'.tr}${navigation["nextRoad"]}", // 进入
style: TextStyle(
fontSize: 14.sp,
fontSize: 12.sp,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.w500,
height: 1.3, // 设置行高,提升多行文本可读性
),
softWrap: true, // 启用软换行
overflow: TextOverflow.visible, // 确保文本完全显示
),
],
),
],
),
// 导航状态信息
if (navigation != null) ...[
SizedBox(height: 6.h),
Container(
padding: EdgeInsets.symmetric(
horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color: Theme.of(context)
.primaryColor
.withOpacity(0.1),
borderRadius: BorderRadius.circular(6.r),
),
child: Text(
"${navigation["distanceText"]}${'enterRoad'.tr}${navigation["nextRoad"]}", // 进入
style: TextStyle(
fontSize: 12.sp,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.w500,
),
),
),
],
],
),
),
// 开始导航按钮 - 绝对定位到右下角
Positioned(
right: 0,
bottom: 0,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context).primaryColor,
Theme.of(context).primaryColor.withOpacity(0.8),
],
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color:
Theme.of(context).primaryColor.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
SizedBox(width: 16.w), // 左右内容之间的间距
// 右侧:开始导航按钮
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context).primaryColor,
Theme.of(context).primaryColor.withOpacity(0.8),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(12.r),
onTap: () {
controller.openNavigation(start, end, "");
},
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 16.w,
vertical: 12.h,
boxShadow: [
BoxShadow(
color: Theme.of(context).primaryColor.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
child: Icon(
Icons.navigation,
color: Colors.white,
size: 20.sp,
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(12.r),
onTap: () {
controller.openNavigation(start, end, "");
},
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 16.w,
vertical: 12.h,
),
child: Icon(
Icons.navigation,
color: Colors.white,
size: 20.sp,
),
),
),
),
),
),
],
),
],
),

2
lib/modules/common/widgets/version_update_dialog.dart

@ -151,7 +151,7 @@ class VersionUpdateDialog extends StatelessWidget {
Expanded(
child: ElevatedButton(
onPressed: () async {
Get.back();
//Get.back();//点击更新之后,弹窗不关闭,增强强制更新的有效性
await _handleUpdate();
onUpdate?.call();
},

60
lib/modules/explore/controllers/explore_controller.dart

@ -76,6 +76,17 @@ class ExploreController extends GetxController {
targetRoute: Routes.translation,
),
// AIFeature(
// title: 'conferenceAssistant'.tr, // 商务会议助手
// description: 'conferenceAssistantInfo'.tr, // 实时记录会议纪要,多语言翻译
// icon: Icons.people,
// startColor: const Color(0xFFab47bc),
// endColor: const Color(0xFFec407a),
// iconBgColor: const Color(0xFFd81b60).withOpacity(0.2),
// iconColor: const Color(0xFFd81b60),
// agentId: 'meeting_assistant',
// targetRoute: Routes.meeting,
// ),
// AIFeature(
// title: 'aiGirlfriend'.tr, // 亲子陪伴
// description: 'aiGirlfriendInfo'.tr, // 智能陪伴,随时聊天
// icon: Icons.favorite,
@ -99,6 +110,20 @@ class ExploreController extends GetxController {
// ),
];
final List<AIFeature> aiFeaturesWide = [
AIFeature(
title: 'conferenceAssistant'.tr, // 商务会议助手
description: 'conferenceAssistantInfo'.tr, // 实时记录会议纪要,多语言翻译
icon: Icons.people,
startColor: const Color(0xFFab47bc),
endColor: const Color(0xFFec407a),
iconBgColor: const Color(0xFFd81b60).withOpacity(0.2),
iconColor: const Color(0xFFd81b60),
agentId: 'meeting_assistant',
targetRoute: Routes.meeting,
),
];
// 热门功能场景列表
final List<FunctionScene> functionScenes = [
FunctionScene(
@ -177,6 +202,36 @@ class ExploreController extends GetxController {
Get.toNamed(feature.targetRoute);
} else if (feature.targetRoute == Routes.agent) {
Get.toNamed(feature.targetRoute);
} else if (feature.targetRoute == Routes.meeting) {
// 跳转到会议页面
Get.toNamed(feature.targetRoute);
}
}
// 点击AI功能(宽版本)
void onAIFeatureWideTap(int index) {
if (index < 0 || index >= aiFeaturesWide.length) return;
final feature = aiFeaturesWide[index];
// 直接根据目标路由进行跳转
if (feature.targetRoute == Routes.meeting) {
Get.toNamed(feature.targetRoute);
} else if (feature.targetRoute == Routes.chat) {
final agent = AgentProvider.getAgentById(feature.agentId);
if (agent != null) {
Get.toNamed(feature.targetRoute, arguments: {
'agentId': agent.id,
'agentName': agent.name,
'agentVoice': agent.voice,
'agentAvatar': agent.avatarUrl,
'agentSubtitle': agent.description,
'systemPrompt': agent.systemPrompt,
'welcomeMessage': agent.welcomeMessage,
});
}
} else if (feature.targetRoute.isNotEmpty) {
Get.toNamed(feature.targetRoute);
}
}
@ -208,6 +263,11 @@ class ExploreController extends GetxController {
// 实际应用中,这里应该跳转到新功能页面
}
// 跳转到翻译统计页面
void goToTranslationStats() {
Get.toNamed(Routes.translationStats);
}
@override
void onClose() {
searchController.dispose();

365
lib/modules/explore/views/explore_view.dart

@ -65,11 +65,11 @@ class ExploreView extends GetView<ExploreController> {
// AI功能列表
SizedBox(height: 24.h),
_buildAIFeaturesSection(isDarkMode),
_buildCustomAILayout(isDarkMode),
// 热门功能场景
SizedBox(height: 24.h),
_buildFunctionScenesSection(isDarkMode),
//_buildFunctionScenesSection(isDarkMode),
// 新功能推荐
SizedBox(height: 24.h),
@ -115,12 +115,12 @@ class ExploreView extends GetView<ExploreController> {
),
child: IconButton(
icon: Icon(
Icons.more_vert,
Icons.analytics_outlined,
size: 16.sp,
color: isDarkMode ? Colors.white : Colors.grey[700],
),
padding: EdgeInsets.zero,
onPressed: () {},
onPressed: controller.goToTranslationStats,
),
),
],
@ -208,37 +208,65 @@ class ExploreView extends GetView<ExploreController> {
),
// 文本内容
Padding(
padding: EdgeInsets.all(24.w),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 22.sp,
fontWeight: FontWeight.bold,
color: Colors.white,
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Padding(
padding: EdgeInsets.all(24.w),
child: Column(
mainAxisSize: MainAxisSize.min, // 使用最小尺寸,避免占用过多空间
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题文本 - 支持多行显示
Text(
title,
style: TextStyle(
fontSize: 22.sp,
fontWeight: FontWeight.bold,
color: Colors.white,
height: 1.2, // 设置行高,提升多行文本可读性
),
softWrap: true, // 启用软换行
overflow: TextOverflow.visible, // 确保文本完全显示
),
),
SizedBox(height: 4.h),
Text(
description,
style: TextStyle(
fontSize: 14.sp,
color: Colors.white.withOpacity(0.8),
SizedBox(height: 6.h), // 稍微增加间距,适应多行布局
// 描述文本 - 支持多行显示
Text(
description,
style: TextStyle(
fontSize: 14.sp,
color: Colors.white.withOpacity(0.8),
height: 1.3, // 设置行高,提升多行文本可读性
),
softWrap: true, // 启用软换行
overflow: TextOverflow.visible, // 确保文本完全显示
),
),
],
],
),
),
),
],
);
}
// AI功能部分
Widget _buildAIFeaturesSection(bool isDarkMode) {
// 自定义AI功能布局:2x2网格,商务会议助手占据下方两个网格位置
Widget _buildCustomAILayout(bool isDarkMode) {
// 找到AI全能助手
final aiAssistantIndex = controller.aiFeatures.indexWhere(
(feature) => feature.agentId == 'personal_assistant',
);
// 找到同声传译
final translationIndex = controller.aiFeatures.indexWhere(
(feature) => feature.agentId == 'simultaneous_interpreter',
);
// 找到商务会议助手(在aiFeaturesWide中)
final meetingFeature = controller.aiFeaturesWide.isNotEmpty
? controller.aiFeaturesWide.first
: null;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -251,20 +279,56 @@ class ExploreView extends GetView<ExploreController> {
),
),
SizedBox(height: 16.h),
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 16.w,
mainAxisSpacing: 16.h,
childAspectRatio: 1.0,
),
itemCount: controller.aiFeatures.length,
itemBuilder: (context, index) {
final feature = controller.aiFeatures[index];
return _buildAIFeatureItem(feature, index, isDarkMode);
},
// 2x2网格布局
Column(
children: [
// 上层:AI全能助手(左)和同声传译(右)
Row(
children: [
// 左上角:AI全能助手
if (aiAssistantIndex != -1)
Expanded(
child: AspectRatio(
aspectRatio: 1.0, // 正方形
child: _buildAIFeatureItem(
controller.aiFeatures[aiAssistantIndex],
aiAssistantIndex,
isDarkMode,
),
),
),
SizedBox(width: 16.w),
// 右上角:同声传译
if (translationIndex != -1)
Expanded(
child: AspectRatio(
aspectRatio: 1.0, // 正方形
child: _buildAIFeatureItem(
controller.aiFeatures[translationIndex],
translationIndex,
isDarkMode,
),
),
),
],
),
SizedBox(height: 16.h),
// 下层:商务会议助手(横跨整行)
if (meetingFeature != null)
AspectRatio(
aspectRatio: 2.0, // 宽高比2:1,横跨两个网格位置
child: _buildAIFeatureItemWideForMeeting(
meetingFeature,
0, // 使用固定索引,因为这是aiFeaturesWide中的项目
isDarkMode,
),
),
],
),
],
);
@ -341,9 +405,202 @@ class ExploreView extends GetView<ExploreController> {
fontSize: 12.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
);
}
//整行布局
Widget _buildAIFeaturesWideSection(bool isDarkMode) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Text(
// 'aiFunction'.tr, // AI功能
// style: TextStyle(
// fontSize: 18.sp,
// fontWeight: FontWeight.w600,
// color: isDarkMode ? Colors.white : Colors.grey[800],
// ),
// ),
// SizedBox(height: 16.h),
// 直接使用 Column 包裹宽版卡片
Column(
children: controller.aiFeaturesWide.map((feature) {
return Padding(
padding: EdgeInsets.only(bottom: 16.h), // 卡片间距
child: _buildAIFeatureItemWide(
feature,
controller.aiFeatures.indexOf(feature),
isDarkMode,
),
);
}).toList(),
),
],
);
}
// AI功能项(宽版本,用于横跨整行的AI全能助手)
Widget _buildAIFeatureItemWide(
AIFeature feature, int index, bool isDarkMode) {
return GestureDetector(
onTap: () => controller.onAIFeatureTap(index),
child: Container(
padding: EdgeInsets.all(20.w),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
feature.startColor.withOpacity(isDarkMode ? 0.3 : 0.3),
feature.endColor.withOpacity(isDarkMode ? 0.3 : 0.3),
],
),
borderRadius: BorderRadius.circular(20.r),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withOpacity(0.2)
: Colors.grey.withOpacity(0.1),
blurRadius: 8.w,
offset: const Offset(0, 4),
),
],
),
child: Row(
children: [
// 图标圆圈
Container(
width: 48.w,
height: 48.w,
decoration: BoxDecoration(
color: feature.iconColor.withValues(alpha: 0.2),
shape: BoxShape.circle,
),
child: Icon(
feature.icon,
size: 24.sp,
color: feature.iconColor,
),
),
SizedBox(width: 16.w),
// 文本内容
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 标题
Text(
feature.title,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.grey[800],
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 4.h),
// 描述
Text(
feature.description,
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
);
}
// AI功能项(宽版本,专门用于商务会议助手)
Widget _buildAIFeatureItemWideForMeeting(
AIFeature feature, int index, bool isDarkMode) {
return GestureDetector(
onTap: () => controller.onAIFeatureWideTap(index),
child: Container(
padding: EdgeInsets.all(20.w),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
feature.startColor.withValues(alpha: isDarkMode ? 0.3 : 0.3),
feature.endColor.withValues(alpha: isDarkMode ? 0.3 : 0.3),
],
),
borderRadius: BorderRadius.circular(20.r),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withValues(alpha: 0.2)
: Colors.grey.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Row(
children: [
// 图标圆圈
Container(
width: 48.w,
height: 48.w,
decoration: BoxDecoration(
color: feature.iconColor.withValues(alpha: 0.2),
shape: BoxShape.circle,
),
child: Icon(
feature.icon,
size: 24.sp,
color: feature.iconColor,
),
),
SizedBox(width: 16.w),
// 文本内容
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 标题
Text(
feature.title,
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.grey[800],
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 4.h),
// 描述
Text(
feature.description,
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
@ -358,15 +615,15 @@ class ExploreView extends GetView<ExploreController> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'popularFunction'.tr, // 热门功能场景
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.grey[800],
),
),
SizedBox(height: 16.h),
// Text(
// 'popularFunction'.tr, // 热门功能场景
// style: TextStyle(
// fontSize: 18.sp,
// fontWeight: FontWeight.w600,
// color: isDarkMode ? Colors.white : Colors.grey[800],
// ),
// ),
// SizedBox(height: 16.h),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
@ -391,13 +648,15 @@ class ExploreView extends GetView<ExploreController> {
child: Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.grey[100],
color: isDarkMode
? Colors.white.withValues(alpha: 0.05)
: Colors.grey[100],
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withOpacity(0.2)
: Colors.grey.withOpacity(0.1),
? Colors.black.withValues(alpha: 0.2)
: Colors.grey.withValues(alpha: 0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),

117
lib/modules/home/controllers/home_controller.dart

@ -5,6 +5,9 @@ import '../../../core/utils/logger.dart';
import 'package:flutter/material.dart';
import '../../../data/services/bluetooth_manager.dart';
import '../../../data/services/ble_manager.dart';
import '../../../data/services/version_update_service.dart';
import '../../common/widgets/version_update_dialog.dart';
import 'package:get_storage/get_storage.dart';
/// 连接状态枚举
enum ConnectionStatus {
@ -50,6 +53,14 @@ class HomeController extends GetxController {
final _bluetoothManager = Get.find<BluetoothManager>();
final _bleManager = Get.find<BleManager>();
final _storage = GetStorage();
// 会话级别的版本检查标记(静态变量,整个应用会话期间保持)
static bool _hasCheckedVersionInSession = false;
// 版本检查相关常量
static const String _lastVersionCheckKey = 'last_version_check_timestamp';
static const int _versionCheckIntervalHours = 24; // 24小时间隔
@override
void onInit() {
super.onInit();
@ -67,6 +78,12 @@ class HomeController extends GetxController {
void _initWithCachedInfo() {
_setupStateListeners();
_checkConnectionStatus();
// 延迟检查版本更新,避免影响页面加载
// 混合检查机制:会话检查 + 24小时间隔检查
Future.delayed(const Duration(seconds: 2), () {
_checkVersionUpdateIfNeeded();
});
}
void _setupStateListeners() {
@ -253,4 +270,104 @@ class HomeController extends GetxController {
? deviceInfo.value!.firmwareVersion
: 'unknown'.tr;
}
/// 混合检查机制:判断是否需要进行版本检查
/// 检查条件:1. 会话内未检查过 OR 2. 距离上次检查超过24小时
void _checkVersionUpdateIfNeeded() {
try {
final now = DateTime.now();
final lastCheckTimestamp = _storage.read(_lastVersionCheckKey);
bool shouldCheck = false;
String reason = '';
// 条件1:会话内未检查过
if (!_hasCheckedVersionInSession) {
shouldCheck = true;
reason = '会话内首次检查';
}
// 条件2:距离上次检查超过24小时
else if (lastCheckTimestamp != null) {
final lastCheckTime = DateTime.fromMillisecondsSinceEpoch(lastCheckTimestamp);
final timeDifference = now.difference(lastCheckTime);
if (timeDifference.inHours >= _versionCheckIntervalHours) {
shouldCheck = true;
reason = '距离上次检查已超过${_versionCheckIntervalHours}小时';
}
}
// 条件3:从未检查过(首次使用应用)
else {
shouldCheck = true;
reason = '首次使用应用';
}
if (shouldCheck) {
Logger.i(_tag, '开始版本检查 - 原因: $reason');
_performVersionCheck();
} else {
Logger.i(_tag, '跳过版本检查 - 会话内已检查且未超过时间间隔');
}
} catch (e) {
Logger.e(_tag, '版本检查判断失败: $e');
}
}
/// 执行版本检查(在后台进行,不阻塞启动流程)
Future<void> _performVersionCheck() async {
try {
final versionService = VersionUpdateService.to;
final updateInfo = await versionService.checkForUpdate();
if (updateInfo.hasUpdate) {
Logger.i(_tag, '发现新版本: ${updateInfo.latestVersion}');
// 显示更新对话框
await VersionUpdateDialog.show(updateInfo);
} else {
Logger.i(_tag, '当前已是最新版本: ${updateInfo.currentVersion}');
}
// 更新检查记录
_updateVersionCheckRecord();
} catch (e) {
Logger.e(_tag, '版本检查失败: $e');
// 即使检查失败,也更新记录,避免频繁重试
_updateVersionCheckRecord();
}
}
/// 更新版本检查记录
void _updateVersionCheckRecord() {
try {
// 更新时间戳
final now = DateTime.now();
_storage.write(_lastVersionCheckKey, now.millisecondsSinceEpoch);
// 标记本次会话已检查过版本
_hasCheckedVersionInSession = true;
Logger.i(_tag, '版本检查记录已更新: ${now.toIso8601String()}');
} catch (e) {
Logger.e(_tag, '更新版本检查记录失败: $e');
}
}
/// 重置版本检查标记(可用于强制重新检查)
/// 注意:这会清除会话标记,但不会清除时间戳记录
static void resetVersionCheckFlag() {
_hasCheckedVersionInSession = false;
Logger.i('HomeCtrl', '版本检查会话标记已重置');
}
/// 完全重置版本检查状态(包括时间戳)
/// 用于测试或特殊情况下强制重新检查
void forceResetVersionCheck() {
try {
_hasCheckedVersionInSession = false;
_storage.remove(_lastVersionCheckKey);
Logger.i(_tag, '版本检查状态已完全重置');
} catch (e) {
Logger.e(_tag, '重置版本检查状态失败: $e');
}
}
}

56
lib/modules/home/views/home_view.dart

@ -173,9 +173,9 @@ class HomeView extends GetView<HomeController> {
borderRadius: BorderRadius.circular(16.r),
child: Image.asset(
isDarkMode
? 'assets/images/headphone_dark.png'
: 'assets/images/headphone_light.png',
height: 180.h,
? 'assets/images/headphone_new3.png'
: 'assets/images/headphone_new4.png',
height: 220.h,
fit: BoxFit.contain,
),
),
@ -519,7 +519,6 @@ class HomeView extends GetView<HomeController> {
Widget _buildDisconnectedInfo(bool isDarkMode) {
return Container(
padding: EdgeInsets.symmetric(vertical: 10.h, horizontal: 12.w),
alignment: Alignment.center,
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.grey[100],
borderRadius: BorderRadius.circular(10.r),
@ -528,26 +527,37 @@ class HomeView extends GetView<HomeController> {
width: 1,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.bluetooth_disabled,
size: 14.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[700],
),
SizedBox(height: 12.h),
Text(
'disconnectedDevice'.tr, // 未连接设备,点击下方按钮连接
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[700],
// height: 1.4,
// fontWeight: FontWeight.w500,
child: IntrinsicHeight(
child: Row(
mainAxisAlignment: MainAxisAlignment.center, // 整体居中对齐
crossAxisAlignment: CrossAxisAlignment.start, // 顶部对齐,适应多行文本
children: [
// 图标容器,与文本第一行基线对齐
Padding(
padding: EdgeInsets.only(top: 1.h), // 微调图标位置,与文本第一行基线对齐
child: Icon(
Icons.bluetooth_disabled,
size: 14.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[700],
),
),
//textAlign: TextAlign.center,
),
],
SizedBox(width: 6.w), // 图标和文本间距
// 文本容器,使用Flexible确保自动换行且保持居中效果
Flexible(
child: Text(
'disconnectedDevice'.tr, // 未连接设备,点击下方按钮连接
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[700],
height: 1.4, // 启用行高,提升多行文本可读性
),
textAlign: TextAlign.center, // 文本居中对齐
softWrap: true, // 启用软换行
overflow: TextOverflow.visible, // 确保文本完全显示
),
),
],
),
),
);
}

71
lib/modules/login/controllers/login_controller.dart

@ -210,12 +210,12 @@ class LoginController extends GetxController {
//如果用户未接受隐私政策,显示弹窗
if (!_hasAcceptedPrivacy.value) {
_showPrivacyDialog();
} else {
_initDeviceInfo();
_initAppInfo(); // 新增:初始化APP信息
_checkGuestLoginConfig(); // 检查游客登录配置
}
});
_initDeviceInfo();
_initAppInfo(); // 新增:初始化APP信息
_checkGuestLoginConfig(); // 检查游客登录配置
}
@override
@ -229,7 +229,6 @@ class LoginController extends GetxController {
Future<void> _checkGuestLoginConfig() async {
try {
final versionService = VersionUpdateService.to;
await versionService.checkForUpdate();
// 先检查版本更新,这会同时设置 isShowTourists 的值
await versionService.checkForUpdate();
@ -388,6 +387,9 @@ class LoginController extends GetxController {
// 用户同意,关闭弹窗并继续
_hasAcceptedPrivacy.value = true;
Get.back();
_initDeviceInfo();
_initAppInfo(); // 新增:初始化APP信息
_checkGuestLoginConfig(); // 检查游客登录配置
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0066CC),
@ -601,13 +603,13 @@ class LoginController extends GetxController {
// 为了演示,我们只是模拟发送
if (isEmialContactController() || isPhoneContactController()) {
_startCountdown();
print(contactController.text);
Logger().d(contactController.text);
var response = await Api.getCode({
"addr": selectedCountryCode + contactController.text,
"vtype": loginType,
});
if (response != null) {
print("请求后台数据验证码params=${response}");
Logger().d("请求后台数据验证码params=$response");
//短信已发送
//EasyLoading.showInfo('codeSend'.tr);
}
@ -632,7 +634,7 @@ class LoginController extends GetxController {
// 更新验证码输入
void updateVerificationCode(int index, String value) {
print("updateVerificationCode=${index}${value}");
Logger().d("updateVerificationCode=$index$value");
if (index >= 0 && index < 4) {
var newList = [..._verificationCode];
newList[index] = value;
@ -665,7 +667,7 @@ class LoginController extends GetxController {
bool isVerificationCodeComplete() {
// 检查每个元素是否为单个字母或数字字符
final text = verificationCodeController.text;
print("数据获取Facebook${text.length}");
Logger().d("数据获取Facebook${text.length}");
return text.length == 4 && RegExp(r'^[0-9]+$').hasMatch(text);
}
@ -712,13 +714,6 @@ class LoginController extends GetxController {
//验证隐私协议勾选状态
if (!_hasAcceptedPrivacy.value) {
// Get.snackbar(
// 'tip'.tr,
// 'pleaseAcceptPrivacyPolicy'.tr,
// snackPosition: SnackPosition.TOP,
// backgroundColor: Colors.red.withOpacity(0.1),
// colorText: Colors.red,
// );
_showPrivacyDialog();
return;
}
@ -736,7 +731,7 @@ class LoginController extends GetxController {
if (!isVerificationCodeComplete()) {
Get.snackbar(
'tip'.tr,
'pleaseEnterVerificationCode'.tr,
'pleaseEnterCompleteCode'.tr,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
@ -783,14 +778,14 @@ class LoginController extends GetxController {
}
case 4: //Facebook登录
{
print("数据获取Facebook");
Logger().d("数据获取Facebook");
final facebook = Facebook();
bool authResult = await facebook.signInWithFacebook();
if (authResult) {
print("数据获取_Facebook.token${facebook.token}");
print("数据获取_Facebook.pictureurl${facebook.pictureurl}");
print("数据获取_Facebook.pictureurl${facebook.email}");
Logger().d("数据获取_Facebook.token${facebook.token}");
Logger().d("数据获取_Facebook.pictureurl${facebook.pictureurl}");
Logger().d("数据获取_Facebook.pictureurl${facebook.email}");
// SettingsController().userName = Facebook().name;
// SettingsController().avatar = Facebook().pictureurl;
// SettingsController().userId = Facebook().id;
@ -813,15 +808,15 @@ class LoginController extends GetxController {
}
case 3: //Google登录
{
print("数据获取_Google()");
Logger().d("数据获取_Google()");
final google = Google();
bool authResult = await google.signInWithGoogle();
print("数据获取_Google().token${authResult}");
Logger().d("数据获取_Google().token${authResult}");
if (authResult) {
print("数据获取_Google.token${google.token}");
print("数据获取_Google.pictureurl${google.pictureurl}");
print("数据获取_Google.email${google.email}");
print("数据获取_Google.email${google.displayName}");
Logger().d("数据获取_Google.token${google.token}");
Logger().d("数据获取_Google.pictureurl${google.pictureurl}");
Logger().d("数据获取_Google.email${google.email}");
Logger().d("数据获取_Google.email${google.displayName}");
// 验证成功,跳转到配对页面
// SettingsController().userName = Google().displayName!;
// //SettingsController().avatar = Google().email;
@ -843,7 +838,7 @@ class LoginController extends GetxController {
}
case 2: //微信登录
{
print("数据获取_FlWX()");
Logger().d("数据获取_FlWX()");
bool authResult = await FlWX().authBy(
NormalAuth(
@ -856,7 +851,7 @@ class LoginController extends GetxController {
if (authResult) {
// 确保 FlWX() 返回的是同一个实例,并且 token 已经被设置
// 这里假设 FlWX().token 在 authBy 成功后被设置
print("数据获取_FlWX().token${FlWX().token}");
Logger().d("数据获取_FlWX().token${FlWX().token}");
// SettingsController().userName = FlWX().nickname;
// SettingsController().avatar = FlWX().headimgurl;
// SettingsController().userId = FlWX().openid;
@ -929,19 +924,19 @@ class LoginController extends GetxController {
"avatar": params["avatar"],
});
if (response != null) {
print("请求后台数据login回复数据=${response}");
Logger().d("请求后台数据login回复数据=$response");
// final instance = await GetStorage();
await _storage.write('loginInfo', jsonEncode(response));
// 2. 将响应数据转换为User对象
try {
print('响应数据: ${response.toString()}'); // 调试输出原始数据
Logger().d('响应数据: ${response.toString()}'); // 调试输出原始数据
if (response['token'].toString() != '') {
// 新增:检查 user 字段是否存在且类型正确
// if (response.containsKey('data') && response['data'] is Map) {
final user = User.fromJson(response);
print('用户初始化成功: ${response['token'].toString()}');
Logger().d('用户初始化成功: ${response['token'].toString()}');
await _storage.write("logintoken", response['token'].toString());
// 存储到 SharedPreferences (仅存储用户数据)
//final prefs = await GetStorage();
@ -958,8 +953,8 @@ class LoginController extends GetxController {
}
} catch (e, stack) {
// 新增:打印堆栈跟踪
print('用户数据处理失败: $e');
print('堆栈跟踪: $stack');
Logger().d('用户数据处理失败: $e');
Logger().d('堆栈跟踪: $stack');
Get.snackbar('错误', '用户数据解析失败,请检查网络');
}
@ -981,18 +976,18 @@ class LoginController extends GetxController {
Future<bool> getAppConfig() async {
try {
print('getAppConfig:进来了');
Logger().d('getAppConfig:进来了');
//if (User.getname() != "") {
final response = await Api.getappconfig();
print('getAppConfig: $response');
Logger().d('getAppConfig: $response');
if (response == null) return false;
print('getAppConfig:进来了');
Logger().d('getAppConfig:进来了');
// 根据API设计调整
final resp = UserGetAppConfigResp.fromJson(response);
AppConfig.initialize(resp);
return true;
} catch (e) {
print('getAppConfig 取失败: $e');
Logger().d('getAppConfig 取失败: $e');
return false;
}
}

14
lib/modules/login/views/login_view.dart

@ -39,13 +39,13 @@ class LoginView extends GetView<LoginController> {
SizedBox(height: 32.h),
_buildWelcomeText(isDarkMode),
// // 输入框和继续按钮
// SizedBox(height: 24.h),
// _buildInputField(isDarkMode),
// SizedBox(height: 16.h),
// _buildVerificationInputField(isDarkMode),
// SizedBox(height: 16.h),
// _buildContinueButton(isDarkMode),
// 输入框和继续按钮
SizedBox(height: 24.h),
_buildInputField(isDarkMode),
SizedBox(height: 16.h),
_buildVerificationInputField(isDarkMode),
SizedBox(height: 16.h),
_buildContinueButton(isDarkMode),
// 社交媒体登录
SizedBox(height: 32.h),

6
lib/modules/meeting/bindings/meeting_binding.dart

@ -1,10 +1,16 @@
import 'package:get/get.dart';
import '../controllers/meeting_controller.dart';
import '../controllers/meeting_home_controller.dart';
import '../controllers/meeting_mine_controller.dart';
import '../../profile/controllers/profile_controller.dart';
class MeetingBinding extends Bindings {
@override
void dependencies() {
// 注册控制器
Get.lazyPut<MeetingController>(() => MeetingController());
Get.lazyPut<MeetingHomeController>(() => MeetingHomeController());
Get.lazyPut<MeetingMineController>(() => MeetingMineController());
Get.lazyPut<ProfileController>(() => ProfileController());
}
}

4
lib/modules/meeting/controllers/meeting_connect_controller.dart

@ -7,10 +7,10 @@ import 'package:path_provider/path_provider.dart';
import 'package:wifi_iot/wifi_iot.dart';
import '../../../core/utils/ftpconnect/ftpconnect.dart';
import 'meeting_controller.dart';
import 'meeting_home_controller.dart';
class MeetingConnectController extends GetxController {
final _meetingController = Get.find<MeetingController>();
final _meetingController = Get.find<MeetingHomeController>();
RxString stateStr = '搜索中...'.obs;
RxBool isRescan = false.obs;

398
lib/modules/meeting/controllers/meeting_controller.dart

@ -1,402 +1,24 @@
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:get/get.dart';
import 'package:just_audio/just_audio.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import '../../../core/utils/logger.dart';
import '../../../core/utils/synchrodata.dart';
import '../../../data/services/ble_manager.dart';
import '../../../data/services/db/sqflite_api.dart';
import '../../../core/utils/permission_util.dart';
import 'package:share_plus/share_plus.dart';
import '../../../data/services/meeting/meeting_task_service.dart';
import '../../../data/services/meeting/meeting_upload_service.dart';
import '../../../routes/app_routes.dart';
import '../views/meeting_home_view.dart';
import '../views/meeting_mine_view.dart';
class MeetingController extends GetxController {
final meetingUploadService = Get.find<MeetingUploadService>();
final meetingTaskService = Get.find<MeetingTaskService>();
final ScrollController listController = ScrollController();
final AudioPlayer _audioPlayer = AudioPlayer();
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
// 是否还有更多数据
bool isGetMore = true;
bool isLoading = false;
int page = 1;
int addQuantity = 0;
List<String> selected = [];
late PageController pageController;
final appDocDir = getApplicationDocumentsDirectory();
List<FileSystemEntity> files = [];
TextEditingController titleController = TextEditingController(); // 录音标题
String titleCSuffix = ""; // 录音标题后缀
RxList dataList = [].obs;
final List<Widget> pageList = [
const MeetingHomeView(),
const MeetingMineView(),
];
// 蓝牙服务
final bleManager = Get.find<BleManager>();
RxInt pageIndex = 0.obs;
@override
void onInit() {
super.onInit();
listController.addListener(_onScroll);
_requestStoragePermission();
_initGetDataList();
addMeetingAudio();
final Map<String, dynamic> args = Get.arguments ?? {};
// 检查参数是否存在且包含有效路径
if (args.isNotEmpty &&
args.containsKey('filePath') &&
args['filePath'] is String &&
args['filePath'].isNotEmpty) {
final String path = args['filePath'];
addAudio(path);
}
}
@override
void onClose() {
listController.dispose();
titleController.dispose();
_audioPlayer.dispose();
super.onClose();
}
void onPopInvokedWithResult(didPop, result) {
Synchrodata.uploadOperationRecords(isSynchro: true);
}
void _onScroll() {
if (isGetMore &&
!isLoading &&
listController.position.pixels >=
listController.position.maxScrollExtent - 100) {
isLoading = true;
page++;
_getDataList();
}
}
void _initGetDataList() async {
await Synchrodata.uploadOperationRecords(isSynchro: true);
_getDataList();
}
// 请求本地数据
void _getDataList() async {
List list = await SqfliteApi.getMeetingList((page - 1) * 10 + addQuantity);
dataList.addAll(list);
addQuantity = 0;
if (list.length < 10) {
isGetMore = false;
} else {
isGetMore = true;
}
isLoading = false;
}
// 请求存储权限
void _requestStoragePermission() async {
PermissionUtil.instance
.requestPermission(
permissionType: Permission.storage,
permissionName: "存储",
explanationText: "我们需要访问存储权限,以便您可以选择并上传音频文件。",
permanentDenialText: "检测到您已永久拒绝存储权限。如需继续使用此功能,请前往应用设置中手动开启存储权限。",
)
.then((granted) {
if (granted) {
Logger.d("Permission", "用户授予了存储权限");
} else {
Logger.d("Permission", "用户未授予存储权限");
}
});
}
void loadTranslatopus() async {
final dir =
Directory("/data/user/0/com.yunqiinnovation.deepsound/files/OpusAudio");
files = dir
.listSync()
.where((f) => f is File && (f.path.endsWith('.opus')))
.toList();
}
void loadTranslatAudio() async {
try {
final appDir = await appDocDir; // 先 await 获取目录
final dir = Directory("${appDir.path}/TranslatAudio");
if (!await dir.exists()) {
await dir.create(recursive: true);
files = [];
return;
}
files = dir
.listSync()
.where((f) =>
f is File &&
(f.path.endsWith('.mp3') ||
f.path.endsWith('.wav') ||
f.path.endsWith('.aac') ||
f.path.endsWith('.m4a') ||
f.path.endsWith('.mp4') ||
f.path.endsWith('.mov')))
.toList();
} catch (e) {
files = [];
}
}
Future<void> shareAllTranslatAudio() async {
final filePath = selected[0];
try {
await Share.shareXFiles(
[XFile(filePath)],
text: '分享会议音频',
);
EasyLoading.showToast('分享完成!');
} catch (e) {
EasyLoading.showToast('分享失败: ${e.toString()}');
}
}
void addMeetingAudio() async {
EasyLoading.show(status: '正在导入本地录音...');
try {
int addUp = 0;
final appDir = await appDocDir;
final sourceDirs = [
Directory("${appDir.path}/MeetingAudio/"),
Directory("${appDir.path}/DestAudio/"),
];
final destDir = Directory("${appDir.path}/LocalAudio/");
if (!await destDir.exists()) {
await destDir.create(recursive: true);
}
List<FileSystemEntity> allFiles = [];
for (final dir in sourceDirs) {
if (await dir.exists()) {
final files =
await dir.list().where((entity) => entity is File).toList();
allFiles.addAll(files);
}
}
for (final fileEntity in allFiles) {
try {
final file = File(fileEntity.path);
final filePath = file.path;
// 设置音频源获取时长
await _audioPlayer.setAudioSource(AudioSource.file(filePath));
int seconds = _audioPlayer.duration?.inSeconds ?? 0;
// 获取文件修改时间
final fileStat = await file.stat();
final creationTime = fileStat.changed.millisecondsSinceEpoch;
// 准备新路径
final fileName = filePath.split('/').last;
final newPath = "${destDir.path}/$fileName";
// 构建数据
Map<String, Object?> data = {
'title': fileName,
'type': 'LOCAL',
'seconds': seconds,
'filepath': newPath, // 存储新路径
'audiourl': '',
'tasktype': 0,
'creationtime': creationTime,
};
// 插入数据库
final meetingId = await SqfliteApi.insertMeeting(data);
if (meetingId != null) {
// 上传OSS
meetingUploadService.addUpload(meetingId, newPath, 'LocalAudio');
// 更新UI数据
dataList.insert(0, {
'id': meetingId,
'taskid': '',
...data,
});
// 移动文件
await file.copy(newPath);
await file.delete();
addQuantity++;
addUp++;
}
} catch (e) {
print("文件处理失败: ${fileEntity.path}, 错误: $e");
}
}
if (addUp > 0) EasyLoading.showSuccess('成功导入本地 $addUp 条录音');
} catch (e) {
EasyLoading.showError('导入失败: $e');
} finally {
EasyLoading.dismiss();
}
}
void addTranslatAudio() async {
EasyLoading.show(status: '正在批量导入...');
for (final filePath in selected) {
try {
await _audioPlayer.setAudioSource(AudioSource.file(filePath));
final fileStat = await File(filePath).stat();
final creationTime = fileStat.changed.millisecondsSinceEpoch; // 时间戳
int seconds = _audioPlayer.duration?.inSeconds ?? 0;
final fileName = filePath.split('/').last;
Map<String, Object?> data = {
'title': fileName,
'type': 'TRANSLAT',
'seconds': seconds,
'filepath': filePath,
'audiourl': '',
'tasktype': 0,
'creationtime': creationTime,
};
final meetingId = await SqfliteApi.insertMeeting(data);
if (meetingId != null) {
meetingUploadService.addUpload(meetingId, filePath, 'TranslatAudio');
dataList.insert(0, {
'id': meetingId,
'taskid': '',
...data,
});
addQuantity++;
}
} catch (e) {
Logger.error('导入文件失败: $filePath, error: $e');
}
}
EasyLoading.dismiss();
}
void addAudio([String? filePath]) async {
EasyLoading.show(status: '正在导入...');
String fileName = "";
bool isParamProvided = filePath != null; // 更清晰的变量名
if (filePath != null) {
// 正确截取文件名(跨平台安全方式)
fileName = path.basename(filePath);
} else {
FilePickerResult? result = await FilePicker.platform.pickFiles(
withData: true,
);
if (result != null) {
PlatformFile file = result.files.first;
filePath = await _saveFile(file);
fileName = file.name;
} else {
EasyLoading.dismiss();
return;
}
}
final meetingId = await insertMeetingAudio(fileName, filePath);
if (isParamProvided) {
Get.toNamed(Routes.meetingDetails, arguments: {
'id': meetingId,
});
}
EasyLoading.dismiss();
}
Future insertMeetingAudio(String fileName, String filePath) async {
await _audioPlayer.setAudioSource(AudioSource.file(filePath));
int seconds = _audioPlayer.duration?.inSeconds ?? 0;
int creationTime = DateTime.now().millisecondsSinceEpoch;
Map<String, Object?> data = {
'title': fileName,
'type': 'IMPORT',
'seconds': seconds,
'filepath': filePath,
'audiourl': '',
'tasktype': 0,
'creationtime': creationTime,
};
final meetingId = await SqfliteApi.insertMeeting(data);
if (meetingId != null) {
meetingUploadService.addUpload(meetingId, filePath, 'ExternalAudio');
dataList.insert(0, {
'id': meetingId,
'taskid': '',
...data,
});
addQuantity++;
}
return meetingId;
}
Future<String> _saveFile(PlatformFile file) async {
final directory = await getApplicationDocumentsDirectory();
final directoryDir = Directory('${directory.path}/ExternalAudio');
if (!await directoryDir.exists()) {
await directoryDir.create(recursive: true);
}
final String filePath = '${directory.path}/ExternalAudio/${file.name}';
final File localFile = File(filePath);
await localFile.writeAsBytes(file.bytes!);
return filePath;
}
// 修改标题
void editMeetingTitle(int id) async {
if (titleController.text.isNotEmpty) {
String title = titleController.text + titleCSuffix;
SqfliteApi.editMeetingTitle(id, {'title': title});
int index = dataList.indexWhere((item) => item['id'] == id);
if (index != -1) {
final dataMap = Map<String, dynamic>.from(dataList[index]);
dataMap['title'] = title;
dataList[index] = dataMap;
}
}
Get.back();
}
// 删除数据
void deleteMeeting(int id, String filepath) async {
dataList.removeWhere((item) => item['id'] == id);
// 删除数据库中的记录
SqfliteApi.deleteMeeting(id);
_deleteFile(filepath);
meetingUploadService.removeUpload(id, isDeleteTask: true);
meetingTaskService.removeTask(id, isDeleteTask: true);
addQuantity--;
}
// 删除文件
Future<void> _deleteFile(String filepath) async {
File file = File(filepath);
bool fileExists = await file.exists();
if (fileExists) {
await file.delete();
}
pageController = PageController(initialPage: 0);
}
}

37
lib/modules/meeting/controllers/meeting_details_controller.dart

@ -25,6 +25,8 @@ class MeetingDetailsController extends GetxController
late AnimationController lottieController;
late final Worker _tasktypeListener;
final _meetingUploadService = Get.find<MeetingUploadService>();
final _meetingTaskService = Get.find<MeetingTaskService>();
@ -49,12 +51,15 @@ class MeetingDetailsController extends GetxController
Rx<MeetingModel> meetingData = MeetingModel().obs;
Rx<MeetingDetailsModel> meetingDetails = MeetingDetailsModel().obs;
RxList textList = [].obs;
String allText = '';
RxBool isSpeakerText = false.obs;
List editSpeakerTextList = [];
RxInt editSpeakerTextId = 0.obs;
String editSpeakerTextChange = '';
int _everType = 99;
RxInt summaryNumberModifications = 0.obs;
RxInt personnelNumberModifications = 0.obs;
String markmapLibJs = '';
String d3Js = '';
String markmapViewJs = '';
@ -81,10 +86,24 @@ class MeetingDetailsController extends GetxController
_meetingTaskService.meetingId = id;
_getDataDetails();
_loadJS();
_tasktypeListener = ever(_meetingTaskService.tasktype, (value) {
meetingDetails.value.tasktype = value;
if (_everType <= value) {
if (value == 2) {
textList.value = _meetingTaskService.textList;
} else if (value == 3) {
meetingDetails.value.summary = _meetingTaskService.summary;
meetingDetails.value.overview = _meetingTaskService.overview;
summaryNumberModifications.value++;
}
}
meetingDetails.refresh();
});
}
@override
void onClose() {
_tasktypeListener.dispose();
_currentDurationSubscription?.cancel();
_playerStateSubscription?.cancel();
tabController.dispose();
@ -103,6 +122,7 @@ class MeetingDetailsController extends GetxController
final data = await SqfliteApi.getMeetingDetails(id);
meetingData.value = MeetingModel.fromJson(data);
meetingDetails.value = MeetingDetailsModel.fromJson(data['details']);
_meetingTaskService.tasktype.value = meetingDetails.value.tasktype;
if (meetingDetails.value.tasktype >= 3) {
dragOffset.value = 0;
isTop.value = true;
@ -227,6 +247,7 @@ class MeetingDetailsController extends GetxController
void summaryCreate() async {
EasyLoading.show();
_everType = 2;
String audiourl = meetingData.value.audiourl;
String taskId = meetingDetails.value.taskid;
await _meetingTaskService.addTask(id, audiourl, taskId);
@ -234,4 +255,18 @@ class MeetingDetailsController extends GetxController
meetingDetails.refresh();
EasyLoading.dismiss();
}
void refreshSummary() async {
String text = '';
for (var element in textList) {
if (element['speaker'].isNotEmpty) {
text += '[${element['speaker']}]: ';
}
text += '${element['content']}\n';
}
_everType = 3;
await SqfliteApi.editMeeting(id, {'tasktype': 2});
_meetingTaskService.tasktype.value = 2;
_meetingTaskService.aiServiceMessage(id, text);
}
}

420
lib/modules/meeting/controllers/meeting_home_controller.dart

@ -0,0 +1,420 @@
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:get/get.dart';
import 'package:just_audio/just_audio.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import '../../../core/utils/logger.dart';
import '../../../core/utils/synchrodata.dart';
import '../../../data/services/ble_manager.dart';
import '../../../data/services/db/sqflite_api.dart';
import '../../../core/utils/permission_util.dart';
import 'package:share_plus/share_plus.dart';
import '../../../data/services/meeting/meeting_task_service.dart';
import '../../../data/services/meeting/meeting_upload_service.dart';
import '../../../routes/app_routes.dart';
import 'meeting_controller.dart';
class MeetingHomeController extends GetxController {
final meetingController = Get.find<MeetingController>();
final meetingUploadService = Get.find<MeetingUploadService>();
final meetingTaskService = Get.find<MeetingTaskService>();
final AudioPlayer _audioPlayer = AudioPlayer();
List<String> selected = [];
final appDocDir = getApplicationDocumentsDirectory();
List<FileSystemEntity> files = [];
TextEditingController titleController = TextEditingController(); // 录音标题
String titleCSuffix = ""; // 录音标题后缀
RxList originalDataList = [].obs;
RxList dataList = [].obs;
RxString filtrateType = ''.obs;
bool isAsc = false;
TextEditingController filtrateController = TextEditingController();
// 蓝牙服务
final bleManager = Get.find<BleManager>();
@override
void onInit() {
super.onInit();
_requestStoragePermission();
_initGetDataList();
addMeetingAudio();
final Map<String, dynamic> args = Get.arguments ?? {};
// 检查参数是否存在且包含有效路径
if (args.isNotEmpty &&
args.containsKey('filePath') &&
args['filePath'] is String &&
args['filePath'].isNotEmpty) {
final String path = args['filePath'];
addAudio(path);
}
}
@override
void onClose() {
titleController.dispose();
filtrateController.dispose();
_audioPlayer.dispose();
super.onClose();
}
void onPopInvokedWithResult(didPop, result) {
Synchrodata.uploadOperationRecords(isSynchro: true);
}
void _initGetDataList() async {
await Synchrodata.uploadOperationRecords(isSynchro: true);
_getDataList();
}
// 请求本地数据
void _getDataList() async {
List list = await SqfliteApi.getMeetingList();
originalDataList.value = List.from(list);
dataList.value = List.from(list);
}
// 请求存储权限
void _requestStoragePermission() async {
PermissionUtil.instance
.requestPermission(
permissionType: Permission.storage,
permissionName: "存储",
explanationText: "我们需要访问存储权限,以便您可以选择并上传音频文件。",
permanentDenialText: "检测到您已永久拒绝存储权限。如需继续使用此功能,请前往应用设置中手动开启存储权限。",
)
.then((granted) {
if (granted) {
Logger.d("Permission", "用户授予了存储权限");
} else {
Logger.d("Permission", "用户未授予存储权限");
}
});
}
void loadTranslatopus() async {
final dir =
Directory("/data/user/0/com.yunqiinnovation.deepsound/files/OpusAudio");
files = dir
.listSync()
.where((f) => f is File && (f.path.endsWith('.opus')))
.toList();
}
void loadTranslatAudio() async {
try {
final appDir = await appDocDir; // 先 await 获取目录
final dir = Directory("${appDir.path}/TranslatAudio");
if (!await dir.exists()) {
await dir.create(recursive: true);
files = [];
return;
}
files = dir
.listSync()
.where((f) =>
f is File &&
(f.path.endsWith('.mp3') ||
f.path.endsWith('.wav') ||
f.path.endsWith('.aac') ||
f.path.endsWith('.m4a') ||
f.path.endsWith('.mp4') ||
f.path.endsWith('.mov')))
.toList();
} catch (e) {
files = [];
}
}
Future<void> shareAllTranslatAudio() async {
final filePath = selected[0];
try {
await Share.shareXFiles(
[XFile(filePath)],
text: '分享会议音频',
);
EasyLoading.showToast('分享完成!');
} catch (e) {
EasyLoading.showToast('分享失败: ${e.toString()}');
}
}
void addMeetingAudio() async {
EasyLoading.show(status: '正在导入本地录音...');
try {
int addUp = 0;
final appDir = await appDocDir;
final sourceDirs = [
Directory("${appDir.path}/MeetingAudio/"),
Directory("${appDir.path}/DestAudio/"),
];
final destDir = Directory("${appDir.path}/LocalAudio/");
if (!await destDir.exists()) {
await destDir.create(recursive: true);
}
List<FileSystemEntity> allFiles = [];
for (final dir in sourceDirs) {
if (await dir.exists()) {
final files =
await dir.list().where((entity) => entity is File).toList();
allFiles.addAll(files);
}
}
for (final fileEntity in allFiles) {
try {
final file = File(fileEntity.path);
final filePath = file.path;
// 设置音频源获取时长
await _audioPlayer.setAudioSource(AudioSource.file(filePath));
int seconds = _audioPlayer.duration?.inSeconds ?? 0;
// 获取文件修改时间
final fileStat = await file.stat();
final creationTime = fileStat.changed.millisecondsSinceEpoch;
// 准备新路径
final fileName = filePath.split('/').last;
final newPath = "${destDir.path}/$fileName";
// 构建数据
Map<String, Object?> data = {
'title': fileName,
'type': 'LOCAL',
'seconds': seconds,
'filepath': newPath, // 存储新路径
'audiourl': '',
'tasktype': 0,
'creationtime': creationTime,
};
// 插入数据库
final meetingId = await SqfliteApi.insertMeeting(data);
if (meetingId != null) {
// 上传OSS
meetingUploadService.addUpload(meetingId, newPath, 'LocalAudio');
originalDataList.insert(0, {
'id': meetingId,
...data,
});
// 更新UI数据
if (isAsc) {
dataList.add({
'id': meetingId,
...data,
});
} else {
dataList.insert(0, {
'id': meetingId,
...data,
});
}
// 移动文件
await file.copy(newPath);
await file.delete();
addUp++;
}
} catch (e) {
print("文件处理失败: ${fileEntity.path}, 错误: $e");
}
}
if (addUp > 0) EasyLoading.showSuccess('成功导入本地 $addUp 条录音');
} catch (e) {
EasyLoading.showError('导入失败: $e');
} finally {
EasyLoading.dismiss();
}
}
void addTranslatAudio() async {
EasyLoading.show(status: '正在批量导入...');
for (final filePath in selected) {
try {
await _audioPlayer.setAudioSource(AudioSource.file(filePath));
final fileStat = await File(filePath).stat();
final creationTime = fileStat.changed.millisecondsSinceEpoch; // 时间戳
int seconds = _audioPlayer.duration?.inSeconds ?? 0;
final fileName = filePath.split('/').last;
Map<String, Object?> data = {
'title': fileName,
'type': 'TRANSLAT',
'seconds': seconds,
'filepath': filePath,
'audiourl': '',
'tasktype': 0,
'creationtime': creationTime,
};
final meetingId = await SqfliteApi.insertMeeting(data);
if (meetingId != null) {
meetingUploadService.addUpload(meetingId, filePath, 'TranslatAudio');
originalDataList.insert(0, {
'id': meetingId,
...data,
});
if (isAsc) {
dataList.add({
'id': meetingId,
...data,
});
} else {
dataList.insert(0, {
'id': meetingId,
...data,
});
}
}
} catch (e) {
Logger.error('导入文件失败: $filePath, error: $e');
}
}
EasyLoading.dismiss();
}
void addAudio([String? filePath]) async {
EasyLoading.show(status: '正在导入...');
String fileName = "";
bool isParamProvided = filePath != null; // 更清晰的变量名
if (filePath != null) {
// 正确截取文件名(跨平台安全方式)
fileName = path.basename(filePath);
} else {
FilePickerResult? result = await FilePicker.platform.pickFiles(
withData: true,
type: FileType.audio,
);
if (result != null) {
PlatformFile file = result.files.first;
filePath = await _saveFile(file);
fileName = file.name;
} else {
EasyLoading.dismiss();
return;
}
}
final meetingId = await insertMeetingAudio(fileName, filePath);
if (isParamProvided) {
Get.toNamed(Routes.meetingDetails, arguments: {
'id': meetingId,
});
}
EasyLoading.dismiss();
}
Future insertMeetingAudio(String fileName, String filePath) async {
await _audioPlayer.setAudioSource(AudioSource.file(filePath));
int seconds = _audioPlayer.duration?.inSeconds ?? 0;
int creationTime = DateTime.now().millisecondsSinceEpoch;
Map<String, Object?> data = {
'title': fileName,
'type': 'IMPORT',
'seconds': seconds,
'filepath': filePath,
'audiourl': '',
'tasktype': 0,
'creationtime': creationTime,
};
final meetingId = await SqfliteApi.insertMeeting(data);
if (meetingId != null) {
meetingUploadService.addUpload(meetingId, filePath, 'ExternalAudio');
originalDataList.insert(0, {
'id': meetingId,
...data,
});
if (isAsc) {
dataList.add({
'id': meetingId,
...data,
});
} else {
dataList.insert(0, {
'id': meetingId,
...data,
});
}
}
return meetingId;
}
Future<String> _saveFile(PlatformFile file) async {
final directory = await getApplicationDocumentsDirectory();
final directoryDir = Directory('${directory.path}/ExternalAudio');
if (!await directoryDir.exists()) {
await directoryDir.create(recursive: true);
}
final String filePath = '${directory.path}/ExternalAudio/${file.name}';
final File localFile = File(filePath);
await localFile.writeAsBytes(file.bytes!);
return filePath;
}
// 修改标题
void editMeetingTitle(int id) async {
if (titleController.text.isNotEmpty) {
String title = titleController.text + titleCSuffix;
SqfliteApi.editMeetingTitle(id, {'title': title});
int originalIndex =
originalDataList.indexWhere((item) => item['id'] == id);
int index = dataList.indexWhere((item) => item['id'] == id);
if (originalIndex != -1) {
final dataMap =
Map<String, dynamic>.from(originalDataList[originalIndex]);
dataMap['title'] = title;
originalDataList[originalIndex] = dataMap;
}
if (index != -1) {
final dataMap = Map<String, dynamic>.from(dataList[index]);
dataMap['title'] = title;
dataList[index] = dataMap;
}
}
Get.back();
}
// 删除数据
void deleteMeeting(int id, String filepath) async {
originalDataList.removeWhere((item) => item['id'] == id);
dataList.removeWhere((item) => item['id'] == id);
// 删除数据库中的记录
SqfliteApi.deleteMeeting(id);
_deleteFile(filepath);
meetingUploadService.removeUpload(id, isDeleteTask: true);
meetingTaskService.removeTask(id, isDeleteTask: true);
}
// 删除文件
Future<void> _deleteFile(String filepath) async {
File file = File(filepath);
bool fileExists = await file.exists();
if (fileExists) {
await file.delete();
}
}
}

121
lib/modules/meeting/controllers/meeting_mine_controller.dart

@ -0,0 +1,121 @@
import 'package:common_utils/common_utils.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../data/services/db/sqflite_api.dart';
import '../../profile/controllers/profile_controller.dart';
import 'meeting_controller.dart';
import 'meeting_home_controller.dart';
class MeetingMineController extends GetxController {
final _meetingController = Get.find<MeetingController>();
final _homeController = Get.find<MeetingHomeController>();
final profileCtrl = Get.find<ProfileController>();
final ScrollController scrollController = ScrollController();
late final Worker _pageIndexListener;
late final Worker _originalDataListener;
bool isStatistics = true;
Map statistics = {};
RxInt days = 0.obs;
RxInt numberFiles = 0.obs;
RxDouble hourage = 0.0.obs;
RxInt continuousDays = 0.obs;
RxInt maxNumberFiles = 0.obs;
RxDouble maxHourage = 0.0.obs;
int averageNumberFiles = 0;
@override
void onInit() {
super.onInit();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (scrollController.hasClients) {
final now = DateTime.now();
if (now.month > 6) {
scrollController.jumpTo(scrollController.position.maxScrollExtent);
}
}
});
_getStatistics();
_pageIndexListener = ever(_meetingController.pageIndex, (value) {
if (isStatistics && value == 2) {
_getStatistics();
}
});
_originalDataListener = ever(_homeController.originalDataList, (value) {
isStatistics = true;
});
}
@override
void onClose() {
_pageIndexListener.dispose();
_originalDataListener.dispose();
scrollController.dispose();
super.onClose();
}
void _getStatistics() async {
if (!isStatistics) return;
List dataList = await SqfliteApi.getMeetingList();
numberFiles.value = dataList.length;
statistics = {};
for (var element in dataList) {
DateTime creationtime =
DateTime.fromMillisecondsSinceEpoch(element['creationtime']);
int dayOfYear = DateUtil.getDayOfYear(creationtime);
if (!statistics.containsKey(dayOfYear)) {
statistics[dayOfYear] = {
'creationtime': creationtime,
'count': 0,
'seconds': 0,
};
}
statistics[dayOfYear]['count']++;
statistics[dayOfYear]['seconds'] += element['seconds'];
}
int currentD = 1;
int maxD = 1;
int previous = 0;
int h = 0;
int maxH = 0;
int maxN = 0;
int minN = 0;
statistics.forEach((key, value) {
h += value['seconds'] as int;
if (key + 1 == previous) {
currentD++;
if (currentD > maxD) {
maxD = currentD;
}
} else {
currentD = 1;
}
previous = key;
if (value['count'] > maxN) {
maxN = value['count'];
}
if (value['count'] < minN || minN == 0) {
minN = value['count'];
}
if (value['seconds'] > maxH) {
maxH = value['seconds'];
}
});
days.value = statistics.length;
hourage.value = double.parse((h / 60 / 60).toStringAsFixed(2));
continuousDays.value = maxD;
maxNumberFiles.value = maxN;
maxHourage.value = double.parse((maxH / 60 / 60).toStringAsFixed(2));
averageNumberFiles = (maxN + minN) ~/ 2;
isStatistics = false;
}
}

10
lib/modules/meeting/controllers/meeting_record_controller.dart

@ -103,9 +103,9 @@ class MeetingRecordController extends GetxController
try {
final bool granted = await PermissionUtil.instance.requestPermission(
permissionType: Permission.microphone,
permissionName: "录音",
explanationText: "我们需要访问麦克风权限,以便您录制音频内容。",
permanentDenialText: "检测到您已永久拒绝录音权限。如需继续使用此功能,请前往应用设置中手动开启麦克风权限。",
permissionName: 'microphonePermission'.tr,
explanationText: 'microphonePermissionExplanation'.tr,
permanentDenialText: 'microphonePermissionPermanentDenial'.tr,
);
if (granted) {
@ -138,7 +138,7 @@ class MeetingRecordController extends GetxController
switch (audioType.value) {
case 0:
fileName.value = "现场录音";
_audioSourceType = false;
_audioSourceType = true;
break;
case 1:
fileName.value = "音、视频录音";
@ -271,7 +271,7 @@ class MeetingRecordController extends GetxController
void _startHardwareServices() {
switch (audioType.value) {
case 0:
// _bleManager.openEncoder();
_bleManager.openEncoder();
break;
case 1:
_bleManager.openDecoder();

21
lib/modules/meeting/controllers/mind_map_controller.dart

@ -18,6 +18,8 @@ class MindMapController extends GetxController {
final double _width = 1.sw - 25.w;
bool isWebViewReady = false;
late final Worker _summaryNumberListener;
@override
void onInit() {
super.onInit();
@ -108,6 +110,12 @@ class MindMapController extends GetxController {
const toolbarElement = document.getElementById('toolbar');
toolbarElement.append(el);
window.updateMarkmap = async function(value) {
const { root } = transformer.transform(value);
await mm.setData(root);
await mm.fit();
}
window.captureImage = async function() {
const markmapElement = document.getElementById('markmap');
markmapElement.style.height = `${_width}px`;
@ -144,6 +152,17 @@ class MindMapController extends GetxController {
)
..setBackgroundColor(Colors.transparent)
..loadHtmlString(html);
_summaryNumberListener =
ever(_meetingDetailsController.summaryNumberModifications, (value) {
webViewController.runJavaScript(
'updateMarkmap(`${_meetingDetailsController.meetingDetails.value.summary}`)');
});
}
@override
void onClose() {
_summaryNumberListener.dispose();
super.onClose();
}
void _messageReceived(message) async {
@ -151,7 +170,7 @@ class MindMapController extends GetxController {
final String base64Str = dataUrl.split(',').last;
final Uint8List bytes = base64.decode(base64Str);
final String fileName =
'${_meetingDetailsController.meetingData.value.title} mindmap.png';
'${_meetingDetailsController.meetingData.value.title}_mindmap.png';
final tempDir = await getTemporaryDirectory();
final File file = File('${tempDir.path}/$fileName');
await file.writeAsBytes(bytes);

3
lib/modules/meeting/model/meeting_model.dart

@ -7,7 +7,6 @@ class MeetingModel {
late int seconds;
late String filepath;
late String audiourl;
late String taskid;
late int tasktype;
late int creationtime;
@ -18,7 +17,6 @@ class MeetingModel {
..seconds = map["seconds"]
..filepath = map["filepath"]
..audiourl = map["audiourl"]
..taskid = map["taskid"]
..tasktype = map["tasktype"]
..creationtime = map["creationtime"];
@ -30,7 +28,6 @@ class MeetingModel {
data['seconds'] = seconds;
data['filepath'] = filepath;
data['audiourl'] = audiourl;
data['taskid'] = taskid;
data['tasktype'] = tasktype;
data['creationtime'] = creationtime;
return data;

63
lib/modules/meeting/utils/meeting_ui_utils.dart

@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
/// 会议模块UI工具类
/// 提供统一的颜色和样式方法,确保UI风格一致性
class MeetingUIUtils {
/// 获取背景颜色
static Color getBackgroundColor(bool isDarkMode) {
return isDarkMode ? Colors.black : Colors.grey[50]!;
}
/// 获取卡片背景颜色
static Color getCardColor(bool isDarkMode) {
return isDarkMode ? const Color(0xFF1A1A1A) : Colors.white;
}
/// 获取半透明白色
static Color getTranslucentWhite(double alpha) {
return Colors.white.withValues(alpha: alpha);
}
/// 获取半透明黑色
static Color getTranslucentBlack(double alpha) {
return Colors.black.withValues(alpha: alpha);
}
/// 获取主文本颜色
static Color getTextColor(bool isDarkMode) {
return isDarkMode ? Colors.white : Colors.grey[800]!;
}
/// 获取次要文本颜色
static Color getSecondaryTextColor(bool isDarkMode) {
return isDarkMode ? Colors.grey[400]! : Colors.grey[600]!;
}
/// 获取卡片阴影
static List<BoxShadow> getCardShadow(bool isDarkMode) {
return [
BoxShadow(
color: isDarkMode
? Colors.black.withValues(alpha: 0.2)
: Colors.black.withValues(alpha: 0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
];
}
/// 获取按钮颜色
static Color getButtonColor(bool isDarkMode) {
return isDarkMode ? const Color(0xFF0066CC) : Colors.blue;
}
/// 获取带透明度的颜色
static Color getColorWithAlpha(Color color, double alpha) {
return color.withValues(alpha: alpha);
}
/// 获取禁用状态的颜色
static Color getDisabledColor(Color color, bool isEnabled) {
return isEnabled ? color : color.withValues(alpha: 0.5);
}
}

5
lib/modules/meeting/views/bottomSheet/ai_bottom_sheet.dart

@ -292,8 +292,8 @@ class _AIBottomSheetState extends State<AIBottomSheet> {
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
const Color(0xFFab47bc).withOpacity(0.1), // 薰衣草紫
const Color(0xFFec407a).withOpacity(0.2), // 品红
const Color(0xFFab47bc).withValues(alpha: 0.1), // 薰衣草紫
const Color(0xFFec407a).withValues(alpha: 0.2), // 品红
],
),
),
@ -320,7 +320,6 @@ class _AIBottomSheetState extends State<AIBottomSheet> {
isDense: true,
fillColor: Colors.transparent,
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
),
onSubmitted: _sendMessage,
),

10
lib/modules/meeting/views/bottomSheet/edit_title_bottom_sheet.dart

@ -2,9 +2,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../../controllers/meeting_controller.dart';
import '../../controllers/meeting_home_controller.dart';
class EditTitleBottomSheet extends GetView<MeetingController> {
class EditTitleBottomSheet extends GetView<MeetingHomeController> {
final int id;
const EditTitleBottomSheet(this.id, {super.key});
@ -53,7 +53,6 @@ class EditTitleBottomSheet extends GetView<MeetingController> {
color: Colors.grey[300],
),
Container(
height: 48.w,
margin: EdgeInsets.symmetric(vertical: 20.w),
alignment: Alignment.center,
decoration: BoxDecoration(
@ -61,7 +60,7 @@ class EditTitleBottomSheet extends GetView<MeetingController> {
borderRadius: BorderRadius.circular(12.w),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
@ -79,9 +78,10 @@ class EditTitleBottomSheet extends GetView<MeetingController> {
color: Colors.black54,
fontSize: 16.sp,
),
isDense: true,
fillColor: Colors.transparent,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 10.w),
contentPadding: EdgeInsets.all(10.w),
),
),
),

284
lib/modules/meeting/views/bottomSheet/filtrate_drawer.dart

@ -1,74 +1,240 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
class FiltrateDrawer extends StatelessWidget {
import '../../controllers/meeting_controller.dart';
import '../../controllers/meeting_home_controller.dart';
class FiltrateDrawer extends StatefulWidget {
const FiltrateDrawer({super.key});
@override
State<FiltrateDrawer> createState() => _FiltrateDrawerState();
}
class _FiltrateDrawerState extends State<FiltrateDrawer> {
final _controller = Get.find<MeetingController>();
final _homeController = Get.find<MeetingHomeController>();
@override
Widget build(BuildContext context) {
return SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.all(20.w),
child: TextField(
decoration: InputDecoration(
hintText: '在所有文件中搜索',
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.w),
borderSide: BorderSide.none,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 10.w),
margin: EdgeInsets.symmetric(vertical: 15.w),
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(12.w),
),
child: Row(
children: [
Icon(
Icons.search,
color: Colors.black87,
size: 20.w,
),
Expanded(
child: TextField(
controller: _homeController.filtrateController,
style: TextStyle(
color: Colors.black87,
fontSize: 14.sp,
),
decoration: InputDecoration(
hintText: '在所有文件中搜索',
hintStyle: TextStyle(
color: Colors.black54,
fontSize: 14.sp,
),
isDense: true,
fillColor: Colors.transparent,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 10.w),
),
onSubmitted: (String value) {
_controller.scaffoldKey.currentState?.closeDrawer();
_homeController.dataList.value =
_homeController.originalDataList.where((i) {
final titleLower = i['title'].trim().toLowerCase();
final queryLower = value.trim().toLowerCase();
bool isType = true;
if (_homeController.filtrateType.value.isNotEmpty) {
isType =
_homeController.filtrateType.value == i['type'];
}
return isType && titleLower.contains(queryLower);
}).toList();
if (_homeController.isAsc) {
_homeController.dataList.value =
_homeController.dataList.reversed.toList();
}
},
),
),
],
),
),
GestureDetector(
onTap: () {
_filtrateDataList('');
},
behavior: HitTestBehavior.translucent,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 10.w),
decoration: _homeController.filtrateType.value == ''
? BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.w),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.06),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
)
: null,
child: Row(
children: [
Icon(
Icons.folder,
color: Colors.red,
size: 20.w,
),
6.horizontalSpace,
Text(
'全部文件(${_homeController.originalDataList.length})',
style: TextStyle(
color: Colors.black87,
fontSize: 14.sp,
),
),
],
),
),
),
Padding(
padding: EdgeInsetsGeometry.symmetric(vertical: 15.w),
child: Divider(
height: 1,
color: Colors.grey[300],
),
),
Padding(
padding: EdgeInsets.only(bottom: 10.w),
child: Text(
'来自',
style: TextStyle(
color: Colors.grey,
fontSize: 14.sp,
fontWeight: FontWeight.bold,
),
),
),
...['IMPORT', 'TRANSLAT', 'LOCAL'].map(
(name) => GestureDetector(
onTap: () {
_filtrateDataList(name);
},
behavior: HitTestBehavior.translucent,
child: Container(
width: double.infinity,
padding:
EdgeInsets.symmetric(horizontal: 10.w, vertical: 10.w),
margin: EdgeInsets.symmetric(vertical: 2.w),
decoration: _homeController.filtrateType.value == name
? BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.w),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.06),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
)
: null,
child: Text(
name,
style: TextStyle(
color: Colors.black87,
fontSize: 14.sp,
),
),
),
),
),
Padding(
padding: EdgeInsets.symmetric(vertical: 10.w),
child: Text(
'排序方式',
style: TextStyle(
color: Colors.grey,
fontSize: 14.sp,
fontWeight: FontWeight.bold,
),
),
),
GestureDetector(
onTap: () {
_controller.scaffoldKey.currentState?.closeDrawer();
_homeController.isAsc = !_homeController.isAsc;
_homeController.dataList.value =
_homeController.dataList.reversed.toList();
},
behavior: HitTestBehavior.translucent,
child: Container(
padding: EdgeInsets.symmetric(vertical: 10.w),
margin: EdgeInsets.symmetric(vertical: 2.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'创建时间',
style: TextStyle(
color: Colors.black87,
fontSize: 14.sp,
),
),
Icon(
_homeController.isAsc
? Icons.keyboard_double_arrow_up
: Icons.keyboard_double_arrow_down,
color: Colors.grey,
size: 20.w,
),
],
),
filled: true,
fillColor: Colors.grey[100],
contentPadding: EdgeInsets.zero,
),
),
),
ListTile(
leading: Icon(Icons.folder, color: Colors.red),
title:
Text('全部文件(4)', style: TextStyle(fontWeight: FontWeight.bold)),
onTap: () {},
),
ListTile(
leading: Icon(Icons.category, color: Colors.blue),
title: Text('未分类(4)'),
onTap: () {},
),
ListTile(
leading: Icon(Icons.delete, color: Colors.teal),
title: Text('回收站'),
onTap: () {},
),
Divider(),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.w),
child: Text('文件夹', style: TextStyle(color: Colors.grey)),
),
...['NOTE', 'CALL', 'NotePin', 'Import', 'Desktop']
.map((name) => ListTile(
title: Text(name),
onTap: () {},
)),
Divider(),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.w),
child: Text('排序方式', style: TextStyle(color: Colors.grey)),
),
ListTile(
title: Text('创建时间'),
trailing: Icon(Icons.arrow_upward),
onTap: () {},
),
ListTile(
title: Text('修改时间'),
trailing: Icon(Icons.arrow_upward),
onTap: () {},
),
],
],
),
),
);
}
void _filtrateDataList(String type) {
_controller.scaffoldKey.currentState?.closeDrawer();
_homeController.filtrateType.value = type;
_homeController.filtrateController.text = '';
if (type.isNotEmpty) {
_homeController.dataList.value = _homeController.originalDataList
.where((i) => i['type'] == type)
.toList();
} else {
_homeController.dataList.value =
List.from(_homeController.originalDataList);
}
if (_homeController.isAsc) {
_homeController.dataList.value =
_homeController.dataList.reversed.toList();
}
}
}

9
lib/modules/meeting/views/bottomSheet/generate_bottom_sheet.dart

@ -6,7 +6,7 @@ import 'package:get_storage/get_storage.dart';
import '../../controllers/meeting_details_controller.dart';
import 'record_language_bottom_sheet.dart';
import '../template_view.dart';
import '../template/template_view.dart';
class GenerateBottomSheet extends StatefulWidget {
final bool isSummarizeAgain;
@ -283,11 +283,10 @@ class _GenerateBottomSheetState extends State<GenerateBottomSheet> {
_storage.write("meeting_template_list", _templateList);
Get.back();
if (widget.isSummarizeAgain) {
_controller.meetingDetails.value.tasktype = 2;
_controller.meetingDetails.value.summary = '';
_controller.meetingDetails.refresh();
_controller.refreshSummary();
} else {
_controller.summaryCreate();
}
_controller.summaryCreate();
},
child: Container(
height: 48.w,

30
lib/modules/meeting/views/bottomSheet/import_audio_bottom_sheet.dart

@ -3,13 +3,14 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../../../../routes/app_routes.dart';
import '../../controllers/meeting_controller.dart';
import '../../controllers/meeting_home_controller.dart';
class ImportAudioBottomSheet extends GetView<MeetingController> {
class ImportAudioBottomSheet extends GetView<MeetingHomeController> {
const ImportAudioBottomSheet({super.key});
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: Column(
@ -24,7 +25,7 @@ class ImportAudioBottomSheet extends GetView<MeetingController> {
'添加音频',
style: TextStyle(
fontSize: 16.sp,
color: Colors.black87,
color: isDarkMode ? Colors.white : Colors.black87,
fontWeight: FontWeight.bold,
),
),
@ -34,7 +35,7 @@ class ImportAudioBottomSheet extends GetView<MeetingController> {
width: 26.w,
height: 26.w,
decoration: BoxDecoration(
color: Colors.white,
color: isDarkMode ? Colors.grey[800] : Colors.white,
borderRadius: BorderRadius.circular(26.r),
),
child: Icon(
@ -49,7 +50,7 @@ class ImportAudioBottomSheet extends GetView<MeetingController> {
),
Divider(
height: 1,
color: Colors.grey[300],
color: isDarkMode ? Colors.grey[700] : Colors.grey[300],
),
SizedBox(height: 16.w),
_importAudioItem(Icons.mic, '现场录音', onTap: () async {
@ -95,7 +96,7 @@ class ImportAudioBottomSheet extends GetView<MeetingController> {
'支持导入单文件时长不超过5小时的音频',
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey[600],
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
),
),
@ -110,14 +111,19 @@ class ImportAudioBottomSheet extends GetView<MeetingController> {
String title, {
VoidCallback? onTap,
}) {
return GestureDetector(
return Builder(
builder: (context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return GestureDetector(
onTap: onTap,
child: Container(
height: 48.w,
padding: EdgeInsets.symmetric(horizontal: 12.w),
margin: EdgeInsets.symmetric(vertical: 4.w),
decoration: BoxDecoration(
color: Colors.grey[50]!.withOpacity(onTap != null ? 1 : 0.5),
color: isDarkMode
? Colors.grey[800]!.withValues(alpha: onTap != null ? 1 : 0.5)
: Colors.grey[50]!.withValues(alpha: onTap != null ? 1 : 0.5),
borderRadius: BorderRadius.circular(12.w),
),
child: Row(
@ -132,18 +138,22 @@ class ImportAudioBottomSheet extends GetView<MeetingController> {
title,
style: TextStyle(
fontSize: 14.sp,
color: Colors.black87.withOpacity(onTap != null ? 1 : 0.5),
color: isDarkMode
? Colors.white.withValues(alpha: onTap != null ? 1 : 0.5)
: Colors.black87.withValues(alpha: onTap != null ? 1 : 0.5),
),
),
),
Icon(
Icons.arrow_forward_ios,
size: 16.w,
color: Colors.grey.withOpacity(onTap != null ? 1 : 0.5),
color: Colors.grey.withValues(alpha: onTap != null ? 1 : 0.5),
),
],
),
),
);
},
);
}
}

17
lib/modules/meeting/views/bottomSheet/more_bottom_sheet.dart

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../../controllers/meeting_controller.dart';
import '../../controllers/meeting_home_controller.dart';
import '../../controllers/meeting_details_controller.dart';
import 'generate_bottom_sheet.dart';
@ -14,7 +14,7 @@ class MoreBottomSheet extends StatefulWidget {
}
class _MoreBottomSheetState extends State<MoreBottomSheet> {
final _controller = Get.find<MeetingController>();
final _controller = Get.find<MeetingHomeController>();
final _detailsController = Get.find<MeetingDetailsController>();
@override
@ -64,7 +64,7 @@ class _MoreBottomSheetState extends State<MoreBottomSheet> {
return InkWell(
onTap: onTap,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 15.w),
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 14.w),
margin: EdgeInsets.only(bottom: 10.w),
decoration: BoxDecoration(
color: onTap != null ? Colors.white : Colors.grey[200],
@ -72,9 +72,12 @@ class _MoreBottomSheetState extends State<MoreBottomSheet> {
),
child: Row(
children: [
Icon(
icon,
color: Colors.black87,
Opacity(
opacity: onTap != null ? 1.0 : 0.5,
child: Icon(
icon,
color: Colors.black87,
),
),
10.horizontalSpace,
Expanded(
@ -82,7 +85,7 @@ class _MoreBottomSheetState extends State<MoreBottomSheet> {
title,
style: TextStyle(
fontSize: 14.sp,
color: Colors.black87,
color: Colors.black87.withValues(alpha: onTap != null ? 1 : 0.5),
),
),
),

114
lib/modules/meeting/views/bottomSheet/navigation_bar_bottom_sheet.dart

@ -0,0 +1,114 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get_navigation/src/extension_navigation.dart';
import 'package:get/state_manager.dart';
import '../../controllers/meeting_home_controller.dart';
import '../../utils/meeting_ui_utils.dart';
class NavigationBarBottomSheet extends GetView<MeetingHomeController> {
const NavigationBarBottomSheet({super.key});
@override
Widget build(BuildContext context) {
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildItem(
context, // 传递 context 参数
'开始录音',
Icons.mic_rounded,
Colors.red,
),
SizedBox(height: 15.w),
_buildItem(
context, // 传递 context 参数
'导入音频',
Icons.file_upload_outlined,
Colors.deepPurple,
onTap: () {
Get.back();
controller.addAudio();
},
),
SizedBox(height: 24.w),
GestureDetector(
onTap: () => Get.back(),
child: Container(
width: 45.w,
height: 45.w,
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.8),
borderRadius: BorderRadius.circular(50.w),
),
child: const Icon(Icons.close, color: Colors.white),
),
),
SizedBox(height: 24.w),
],
),
);
}
Widget _buildItem(
BuildContext context, // 添加 context 参数
String title,
IconData icon,
Color color, {
Function()? onTap,
}) {
final isDarkMode =
Theme.of(context).brightness == Brightness.dark; // 在方法内部获取 isDarkMode
return GestureDetector(
onTap: onTap,
child: Container(
width: 220.w,
height: 60.w,
padding: EdgeInsets.all(5.w),
decoration: BoxDecoration(
color: isDarkMode
? Colors.grey[800]!.withValues(alpha: onTap != null ? 1 : 0.5)
: Colors.white.withValues(alpha: onTap != null ? 1 : 0.5),
borderRadius: BorderRadius.circular(60.w),
),
child: Row(
children: [
Container(
width: 46.w,
height: 46.w,
margin: EdgeInsets.only(right: 5.w),
decoration: BoxDecoration(
color: color.withValues(alpha: onTap != null ? 0.3 : 0.2),
borderRadius: BorderRadius.circular(60.w),
),
child: Icon(
icon,
color: color.withValues(alpha: onTap != null ? 1 : 0.5),
),
),
Expanded(
child: Text(
title,
style: TextStyle(
fontSize: 15.sp,
color: isDarkMode
? Colors.white.withValues(alpha: onTap != null ? 1 : 0.5)
: Colors.black87
.withValues(alpha: onTap != null ? 1 : 0.5),
fontWeight: FontWeight.bold,
),
),
),
Icon(
Icons.arrow_forward_ios,
size: 16.w,
color: Colors.grey.withOpacity(onTap != null ? 1 : 0.5),
),
5.horizontalSpace,
],
),
),
);
}
}

164
lib/modules/meeting/views/bottomSheet/second_level_bottom_sheet.dart

@ -0,0 +1,164 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
class SecondLevelBottomSheet extends StatefulWidget {
final String title;
final String subtitle;
final Function(int index) onTap;
final List children;
const SecondLevelBottomSheet(
this.title, this.subtitle, this.onTap, this.children,
{super.key});
@override
State<SecondLevelBottomSheet> createState() => _SecondLevelBottomSheetState();
}
class _SecondLevelBottomSheetState extends State<SecondLevelBottomSheet> {
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: EdgeInsets.symmetric(vertical: 10.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
widget.title,
style: TextStyle(
fontSize: 16.sp,
color: Colors.black87,
fontWeight: FontWeight.bold,
),
),
GestureDetector(
onTap: () => Get.back(),
child: Container(
width: 26.w,
height: 26.w,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(26.r),
),
child: Icon(
Icons.close,
size: 18.w,
color: Colors.grey[400],
),
),
)
],
),
),
Divider(
height: 1,
color: Colors.grey[300],
),
Padding(
padding: EdgeInsetsGeometry.only(
top: 20.w,
bottom: 10.w,
left: 12.w,
),
child: Text(
widget.subtitle,
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey[600],
),
),
),
Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.r),
),
child: Column(
children: List.generate(widget.children.length, (int index) {
Map item = widget.children[index];
return InkWell(
onTap: () {
setState(() {
_selectedIndex = index;
});
},
child: Container(
padding:
EdgeInsets.symmetric(horizontal: 12.w, vertical: 14.w),
decoration: BoxDecoration(
border: item['isBorder']
? Border(
bottom: BorderSide(
width: 0.5,
color: Colors.grey[300]!,
),
)
: null,
),
child: Row(
children: [
Icon(
item['icon'],
color: Colors.black87,
),
10.horizontalSpace,
Expanded(
child: Text(
item['title'],
style: TextStyle(
fontSize: 14.sp,
color: Colors.black87,
),
),
),
if (_selectedIndex == index)
const Icon(
Icons.check,
color: Colors.black87,
),
],
),
),
);
}),
),
),
Padding(
padding: EdgeInsets.symmetric(vertical: 20.w),
child: GestureDetector(
onTap: () {
Get.back();
widget.onTap(_selectedIndex);
},
child: Container(
height: 48.w,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(12.r),
),
child: Text(
'导出',
style: TextStyle(
fontSize: 16.sp,
color: Colors.white,
),
),
),
),
),
],
),
);
}
}

90
lib/modules/meeting/views/bottomSheet/share_bottom_sheet.dart

@ -7,6 +7,7 @@ import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:htmltopdfwidgets/htmltopdfwidgets.dart' as pdf;
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import 'package:webview_flutter/webview_flutter.dart';
@ -14,6 +15,7 @@ import 'package:webview_flutter/webview_flutter.dart';
import '../../../../core/utils/synchrodata.dart';
import '../../controllers/meeting_details_controller.dart';
import '../../controllers/mind_map_controller.dart';
import 'second_level_bottom_sheet.dart';
class ShareBottomSheet extends StatefulWidget {
const ShareBottomSheet({super.key});
@ -141,7 +143,7 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
Get.back();
final tempDir = await getTemporaryDirectory();
final file = File(
'${tempDir.path}/${_controller.meetingData.value.title} speaker.txt');
'${tempDir.path}/${_controller.meetingData.value.title}_speaker.txt');
String allText = _getAllText();
await file.writeAsString(allText);
_share(file.path, '分享会议转写');
@ -151,12 +153,46 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
onTap: _controller.meetingDetails.value.tasktype >= 3
? () async {
Get.back();
final tempDir = await getTemporaryDirectory();
final file = File(
'${tempDir.path}/${_controller.meetingData.value.title} summary.txt');
await file.writeAsString(
_controller.meetingDetails.value.summary);
_share(file.path, '分享会议总结');
Get.bottomSheet(
SecondLevelBottomSheet(
'导出总结',
'导出为',
(int index) async {
if (index == 0) {
// 导出TXT
final tempDir =
await getTemporaryDirectory();
final file = File(
'${tempDir.path}/${_controller.meetingData.value.title}_summary.txt');
await file.writeAsString(_controller
.meetingDetails.value.summary);
_share(file.path, '分享会议总结');
} else {
// 导出PDF
_exportMarkdownPdf();
}
},
const [
{
'icon': Icons.text_snippet,
'title': 'TXT',
'isBorder': true,
},
{
'icon': Icons.picture_as_pdf,
'title': 'PDF',
'isBorder': false,
},
],
),
isScrollControlled: true,
backgroundColor: Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
);
}
: null),
_shareItem(
@ -218,12 +254,13 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
return InkWell(
onTap: onTap,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 15.w),
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 14.w),
decoration: BoxDecoration(
color: onTap != null ? null : Colors.grey[200],
border: isBorder
? Border(
bottom: BorderSide(
width: 0.5,
color: Colors.grey[300]!,
),
)
@ -231,9 +268,12 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
),
child: Row(
children: [
Icon(
icon,
color: Colors.black87,
Opacity(
opacity: onTap != null ? 1.0 : 0.5,
child: Icon(
icon,
color: Colors.black87,
),
),
10.horizontalSpace,
Expanded(
@ -241,7 +281,7 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
title,
style: TextStyle(
fontSize: 14.sp,
color: Colors.black87,
color: Colors.black87.withValues(alpha: onTap != null ? 1 : 0.5),
),
),
),
@ -251,7 +291,7 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
);
}
void _share(String filepath, String title) async {
Future<void> _share(String filepath, String title) async {
final result = await SharePlus.instance.share(ShareParams(
text: title,
files: [XFile(filepath)],
@ -268,7 +308,7 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
(value) => Get.snackbar(
'success'.tr, // 成功
'复制成功',
backgroundColor: Colors.blue.withOpacity(0.8),
backgroundColor: Colors.blue.withValues(alpha: 0.8),
colorText: Colors.white,
),
);
@ -297,4 +337,26 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
await file.writeAsBytes(byteData.buffer.asUint8List());
return XFile(file.path);
}
Future<void> _exportMarkdownPdf() async {
EasyLoading.show(status: '正在导出PDF...');
final tempDir = await getTemporaryDirectory();
final file = File(
'${tempDir.path}/${_controller.meetingData.value.title}_summary.pdf');
final List<pdf.Widget> markdownWidgets =
await pdf.HTMLToPdf().convertMarkdown(
_controller.meetingDetails.value.summary,
fontFallback: [
pdf.Font.ttf(
await rootBundle.load('assets/fonts/NotoSansSC-Regular.ttf')),
],
);
final markdownPdf = pdf.Document();
markdownPdf.addPage(pdf.MultiPage(
build: (context) => markdownWidgets,
));
await file.writeAsBytes(await markdownPdf.save());
await _share(file.path, '分享会议总结');
EasyLoading.dismiss();
}
}

2
lib/modules/meeting/views/bottomSheet/speaker_all_bottom_sheet.dart

@ -576,10 +576,12 @@ class _SpeakerAllBottomSheetState extends State<SpeakerAllBottomSheet> {
'personnel': personnel,
});
_controller.meetingDetails.value.summary = summaryText;
_controller.summaryNumberModifications.value++;
} else {
SqfliteApi.editMeeting(_controller.id, {'personnel': personnel});
}
_controller.meetingDetails.value.personnel = personnel;
_controller.personnelNumberModifications.value++;
_controller.meetingDetails.refresh();
_storage.write("speaker_list", _historySpeakerList);
Get.back();

1
lib/modules/meeting/views/bottomSheet/speaker_bottom_sheet.dart

@ -382,6 +382,7 @@ class _SpeakerBottomSheetState extends State<SpeakerBottomSheet> {
'personnel': personnel,
});
_controller.meetingDetails.value.summary = summaryText;
_controller.summaryNumberModifications.value++;
} else {
SqfliteApi.editMeeting(_controller.id, {'personnel': personnel});
}

383
lib/modules/meeting/views/bottomSheet/statistics_bottom_sheet.dart

@ -0,0 +1,383 @@
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:image_gallery_saver_plus/image_gallery_saver_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import '../../../profile/controllers/profile_controller.dart';
import '../../controllers/meeting_mine_controller.dart';
class StatisticsBottomSheet extends StatefulWidget {
const StatisticsBottomSheet({super.key});
@override
State<StatisticsBottomSheet> createState() => _StatisticsBottomSheetState();
}
class _StatisticsBottomSheetState extends State<StatisticsBottomSheet> {
final _mineController = Get.find<MeetingMineController>();
final _profileCtrl = Get.find<ProfileController>();
final GlobalKey _posterKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Stack(
children: [
GestureDetector(
onTap: () => Get.back(),
behavior: HitTestBehavior.translucent,
child: SizedBox(
width: 1.sw,
height: 1.sh,
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 10.w),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
RepaintBoundary(
key: _posterKey,
child: Container(
padding:
EdgeInsets.symmetric(horizontal: 15.w, vertical: 20.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.w),
image: const DecorationImage(
alignment: Alignment.topCenter,
image: AssetImage('assets/images/mine-background.png'),
fit: BoxFit.fitWidth,
),
),
child: Column(
children: [
Container(
width: 50.w,
height: 50.w,
margin: EdgeInsets.only(bottom: 10.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(50.w),
),
child: CircleAvatar(
radius: 25.w,
backgroundImage: _profileCtrl
.localAvatar.value.isNotEmpty
? FileImage(File(_profileCtrl.localAvatar.value))
: (_profileCtrl.userInfo.value.avatar.isNotEmpty
? NetworkImage(
_profileCtrl.userInfo.value.avatar)
: null) as ImageProvider<Object>?,
backgroundColor: Colors.grey[200],
child: (_profileCtrl.localAvatar.value.isEmpty &&
_profileCtrl.userInfo.value.avatar.isEmpty)
? Icon(
Icons.person,
size: 25.r,
color: Colors.grey[600],
)
: null,
),
),
Text(
_profileCtrl.userInfo.value.name,
style: TextStyle(
color: Colors.black87,
fontSize: 20.sp,
fontWeight: FontWeight.bold,
),
),
Padding(
padding: EdgeInsets.symmetric(vertical: 20.w),
child: Wrap(
spacing: 5.w,
runSpacing: 5.w,
children: [
_item(
'天',
'${_mineController.days.value}',
Colors.amber,
),
_item(
'文件',
'${_mineController.numberFiles.value}',
Colors.blueGrey,
),
_item(
'小时',
'${_mineController.hourage.value}',
Colors.cyan,
),
_item(
'连续录音天数',
'${_mineController.continuousDays.value}',
Colors.deepOrangeAccent,
),
_item(
'单日最多录音',
'${_mineController.maxNumberFiles.value}',
Colors.teal,
),
_item(
'单日最长录音',
'${_mineController.maxHourage.value}',
Colors.pink,
),
],
),
),
Row(
children: [
Text(
'过去12个月的热力图',
style: TextStyle(
color: Colors.grey,
fontSize: 12.sp,
),
),
const Spacer(),
Text(
'较冷',
style: TextStyle(
color: Colors.grey,
fontSize: 12.sp,
),
),
5.horizontalSpace,
Container(
width: 10.w,
height: 10.w,
margin: EdgeInsets.symmetric(horizontal: 1.w),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(2.w),
),
),
Container(
width: 10.w,
height: 10.w,
margin: EdgeInsets.symmetric(horizontal: 1.w),
decoration: BoxDecoration(
color: Colors.red[100],
borderRadius: BorderRadius.circular(2.w),
),
),
Container(
width: 10.w,
height: 10.w,
margin: EdgeInsets.symmetric(horizontal: 1.w),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(2.w),
),
),
5.horizontalSpace,
Text(
'较热',
style: TextStyle(
color: Colors.grey,
fontSize: 12.sp,
),
),
],
),
Container(
height: 50.w,
alignment: Alignment.center,
child: GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 52,
mainAxisSpacing: 1.w,
crossAxisSpacing: 1.w,
),
itemCount: 364,
itemBuilder: (BuildContext context, int index) {
final col = index % 52;
final row = index ~/ 52;
final vIndex = row + col * 7;
int type = 0;
if (_mineController.statistics
.containsKey(vIndex + 1)) {
if (_mineController.statistics[vIndex + 1]
['count'] <=
_mineController.averageNumberFiles) {
type = 1;
} else {
type = 2;
}
}
return Container(
color: type == 0
? Colors.grey[200]
: type == 1
? Colors.red[100]
: Colors.red,
);
},
),
),
DefaultTextStyle(
style: TextStyle(
color: Colors.grey,
fontSize: 10.sp,
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text('Jan'),
Text('Feb'),
Text('Mar'),
Text('Apr'),
Text('May'),
Text('Jun'),
Text('Jul'),
Text('Aug'),
Text('Sep'),
Text('Oct'),
Text('Nov'),
Text('Dec'),
],
),
),
],
),
),
),
SizedBox(height: 10.w),
Row(
children: [
_button(Icons.share, '分享图片', Colors.deepOrange, () async {
EasyLoading.show();
Uint8List pngBytes = await _generate();
final tempDir = await getTemporaryDirectory();
final String fileName =
'share_${DateTime.now().millisecondsSinceEpoch}.png';
final file = File('${tempDir.path}/$fileName');
await file.writeAsBytes(pngBytes);
final result = await SharePlus.instance.share(ShareParams(
text: '分享 DEAPSOUND',
files: [XFile(file.path)],
));
if (result.status == ShareResultStatus.success) {
Get.back();
EasyLoading.showToast('分享完成!');
}
EasyLoading.dismiss();
}),
10.horizontalSpace,
_button(Icons.download, '保存图片', Colors.cyan, () async {
EasyLoading.show();
Uint8List pngBytes = await _generate();
final result = await ImageGallerySaverPlus.saveImage(
pngBytes,
quality: 100,
);
if (result['isSuccess']) {
Get.back();
EasyLoading.showToast('保存成功');
} else {
EasyLoading.showToast('保存失败');
}
EasyLoading.dismiss();
}),
],
),
],
),
),
],
);
}
Widget _item(String unit, String amount, Color color) {
return Container(
width: 105.w,
height: 70.w,
padding: EdgeInsets.all(10.w),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(8.w),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
unit,
style: TextStyle(
color: Colors.grey,
fontSize: 12.sp,
),
),
Text(
amount,
style: TextStyle(
color: Colors.black87,
fontSize: 18.sp,
fontWeight: FontWeight.bold,
),
),
],
),
);
}
Widget _button(IconData icon, String title, Color color, Function() onTap) {
return Expanded(
child: GestureDetector(
onTap: onTap,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.w),
),
child: Container(
height: 60.w,
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(16.w),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: 25.w,
),
5.horizontalSpace,
Text(
title,
style: TextStyle(
color: Colors.black87,
fontSize: 16.sp,
),
),
],
),
),
),
),
);
}
Future<Uint8List> _generate() async {
RenderRepaintBoundary boundary =
_posterKey.currentContext!.findRenderObject()! as RenderRepaintBoundary;
ui.Image image = await boundary.toImage(pixelRatio: 6.0);
ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png);
Uint8List pngBytes = byteData!.buffer.asUint8List();
return pngBytes;
}
}

60
lib/modules/meeting/views/meeting_connect_view.dart

@ -3,6 +3,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../controllers/meeting_connect_controller.dart';
import '../utils/meeting_ui_utils.dart';
class MeetingConnectView extends GetView<MeetingConnectController> {
const MeetingConnectView({super.key});
@ -12,7 +13,7 @@ class MeetingConnectView extends GetView<MeetingConnectController> {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
backgroundColor: MeetingUIUtils.getBackgroundColor(isDarkMode),
appBar: _buildAppBar(isDarkMode),
body: _buildBody(isDarkMode),
);
@ -105,34 +106,39 @@ class MeetingConnectView extends GetView<MeetingConnectController> {
}
Widget _button(String text, IconData icon, {Function()? onTap}) {
return GestureDetector(
onTap: onTap,
child: Container(
width: 150.w,
height: 42.w,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(42.w),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
color: Colors.white,
size: 20.sp,
return Builder(
builder: (context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return GestureDetector(
onTap: onTap,
child: Container(
width: 150.w,
height: 42.w,
decoration: BoxDecoration(
color: MeetingUIUtils.getButtonColor(isDarkMode),
borderRadius: BorderRadius.circular(42.w),
),
5.horizontalSpace,
Text(
text,
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
color: Colors.white,
size: 20.sp,
),
5.horizontalSpace,
Text(
text,
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
),
),
],
),
],
),
),
),
);
},
);
}
}

724
lib/modules/meeting/views/meeting_details_view.dart

@ -9,6 +9,7 @@ import 'package:lottie/lottie.dart';
import '../../../core/theme/app_colors.dart';
import '../controllers/meeting_details_controller.dart';
import '../utils/meeting_ui_utils.dart';
import 'tabs/mind_map_tab.dart';
import 'bottomSheet/more_bottom_sheet.dart';
import 'tabs/overview_tab.dart';
@ -25,215 +26,226 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
return PopScope(
onPopInvokedWithResult: controller.popInvokedWithResult,
child: Obx(
() => Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
appBar: _buildAppBar(context, isDarkMode),
body: _buildBody(isDarkMode),
),
child: Scaffold(
backgroundColor: MeetingUIUtils.getBackgroundColor(isDarkMode),
appBar: _buildAppBar(context, isDarkMode),
body: _buildBody(isDarkMode),
),
);
}
// 构建应用栏
PreferredSizeWidget _buildAppBar(BuildContext context, bool isDarkMode) {
return AppBar(
backgroundColor:
controller.isTop.value ? Colors.grey[50] : Colors.grey[200],
surfaceTintColor: Colors.transparent,
elevation: 0.5,
leading: controller.isSpeakerText.value
? GestureDetector(
onTap: () {
controller.isSpeakerText.value = false;
controller.editSpeakerTextId.value = 0;
controller.editSpeakerTextList = [];
},
child: Center(
child: Text(
'cancel'.tr,
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
fontSize: 16.sp,
return PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight),
child: Obx(
() => AppBar(
backgroundColor:
isDarkMode
? (controller.isTop.value ? Colors.black : Colors.grey[900])
: (controller.isTop.value ? Colors.grey[50] : Colors.grey[200]),
surfaceTintColor: Colors.transparent,
elevation: 0.5,
leading: controller.isSpeakerText.value
? GestureDetector(
onTap: () {
controller.isSpeakerText.value = false;
controller.editSpeakerTextId.value = 0;
controller.editSpeakerTextList = [];
},
child: Center(
child: Text(
'cancel'.tr,
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
fontSize: 16.sp,
),
),
),
)
: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: isDarkMode ? Colors.white : Colors.black,
size: 20.sp,
),
onPressed: () => Get.back(),
),
),
)
: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: isDarkMode ? Colors.white : Colors.black,
size: 20.sp,
),
onPressed: () => Get.back(),
),
title: Opacity(
opacity: controller.isTop.value ? 1.0 : 0.0,
child: Container(
width: 170.w,
padding: EdgeInsets.symmetric(horizontal: 2.w, vertical: 3.w),
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(30.r),
),
child: Row(
children: [
GestureDetector(
onTap: () {
if (controller.meetingData.value.filepath.isEmpty) {
controller.downloadAudio();
} else {
if (controller.isPlay.value) {
controller.playerController.pausePlayer();
} else {
controller.playerController.startPlayer();
}
}
},
child: Obx(
() => !controller.isLoading.value &&
controller.meetingData.value.filepath.isEmpty
? Obx(
() => Stack(
alignment: Alignment.center,
children: [
if (controller.isDownload.value)
SizedBox(
width: 26.w,
height: 26.w,
child: CircularProgressIndicator(
value: controller.downloadProgress.value,
valueColor:
const AlwaysStoppedAnimation<Color>(
Colors.red),
strokeWidth: 2.w,
),
title: Obx(
() => Opacity(
opacity:
(1 - (controller.dragOffset.value / 100.w)).clamp(0.0, 1.0),
child: Container(
width: 170.w,
padding: EdgeInsets.symmetric(horizontal: 2.w, vertical: 3.w),
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(30.r),
),
child: Row(
children: [
GestureDetector(
onTap: () {
if (controller.meetingData.value.filepath.isEmpty) {
controller.downloadAudio();
} else {
if (controller.isPlay.value) {
controller.playerController.pausePlayer();
} else {
controller.playerController.startPlayer();
}
}
},
child: Obx(
() => !controller.isLoading.value &&
controller.meetingData.value.filepath.isEmpty
? Obx(
() => Stack(
alignment: Alignment.center,
children: [
if (controller.isDownload.value)
SizedBox(
width: 26.w,
height: 26.w,
child: CircularProgressIndicator(
value:
controller.downloadProgress.value,
valueColor:
const AlwaysStoppedAnimation<
Color>(Colors.red),
strokeWidth: 2.w,
),
),
Icon(
Icons.download_for_offline,
color: Colors.black,
size: 30.w,
),
],
),
Icon(
Icons.download_for_offline,
)
: Icon(
controller.isPlay.value
? Icons.pause_circle
: Icons.play_circle,
color: Colors.black,
size: 30.w,
),
],
),
),
4.horizontalSpace,
GestureDetector(
onTap: () {
if (controller.isSpeakerText.value) return;
controller.dragOffset.value = 210.w;
controller.isTop.value = false;
},
behavior: HitTestBehavior.translucent,
child: SizedBox(
width: 130.w,
height: 30.w,
child: FittedBox(
fit: BoxFit.scaleDown,
child: AudioFileWaveforms(
size: Size(350.w, 30.w),
playerController: controller.playerController,
waveformData: List.generate(88, (int index) {
return 0.01 + Random().nextDouble() * 0.09;
}),
waveformType: WaveformType.fitWidth,
enableSeekGesture: false,
playerWaveStyle: PlayerWaveStyle(
spacing: 4.w,
waveThickness: 2.w,
fixedWaveColor: Colors.grey[400]!,
liveWaveColor: Colors.red,
seekLineColor: Colors.red,
scaleFactor: 200,
),
),
)
: Icon(
controller.isPlay.value
? Icons.pause_circle
: Icons.play_circle,
color: Colors.black,
size: 30.w,
),
),
),
4.horizontalSpace,
GestureDetector(
onTap: () {
if (controller.isSpeakerText.value) return;
controller.dragOffset.value = 210.w;
controller.isTop.value = false;
},
child: SizedBox(
width: 130.w,
child: FittedBox(
fit: BoxFit.scaleDown,
child: AudioFileWaveforms(
size: Size(350.w, 30.w),
playerController: controller.playerController,
waveformData: List.generate(88, (int index) {
return 0.01 + Random().nextDouble() * 0.09;
}),
waveformType: WaveformType.fitWidth,
enableSeekGesture: false,
playerWaveStyle: PlayerWaveStyle(
spacing: 4.w,
waveThickness: 2.w,
fixedWaveColor: Colors.grey[400]!,
liveWaveColor: Colors.red,
seekLineColor: Colors.red,
scaleFactor: 200,
),
),
),
),
),
],
),
),
),
centerTitle: true,
actions: [
if (controller.isSpeakerText.value)
GestureDetector(
onTap: controller.editSpeakerText,
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: 12.w,
vertical: 6.w,
),
child: Text(
'save'.tr,
style: TextStyle(
color: isDarkMode ? AppColors.primary : Colors.blue,
fontSize: 16.sp,
],
),
),
),
),
if (!controller.isSpeakerText.value)
IconButton(
icon: Icon(
Icons.ios_share,
color: isDarkMode ? Colors.white : Colors.black,
size: 22.sp,
),
onPressed: () {
Get.bottomSheet(
const ShareBottomSheet(),
isScrollControlled: true,
backgroundColor: Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
centerTitle: true,
actions: [
if (controller.isSpeakerText.value)
GestureDetector(
onTap: controller.editSpeakerText,
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: 12.w,
vertical: 6.w,
),
child: Text(
'save'.tr,
style: TextStyle(
color: isDarkMode ? AppColors.primary : Colors.blue,
fontSize: 16.sp,
),
),
),
);
},
),
if (!controller.isSpeakerText.value)
IconButton(
icon: Container(
width: 22.w,
height: 22.w,
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20.r),
border: Border.all(
),
if (!controller.isSpeakerText.value)
IconButton(
icon: Icon(
Icons.ios_share,
color: isDarkMode ? Colors.white : Colors.black,
width: 2,
size: 22.sp,
),
onPressed: () {
Get.bottomSheet(
const ShareBottomSheet(),
isScrollControlled: true,
backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
);
},
),
child: Icon(
Icons.more_horiz,
color: isDarkMode ? Colors.white : Colors.black,
size: 16.sp,
),
),
onPressed: () {
Get.bottomSheet(
const MoreBottomSheet(),
isScrollControlled: true,
backgroundColor: Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
if (!controller.isSpeakerText.value)
IconButton(
icon: Container(
width: 22.w,
height: 22.w,
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20.r),
border: Border.all(
color: isDarkMode ? Colors.white : Colors.black,
width: 2,
),
),
child: Icon(
Icons.more_horiz,
color: isDarkMode ? Colors.white : Colors.black,
size: 16.sp,
),
),
);
},
),
],
onPressed: () {
Get.bottomSheet(
const MoreBottomSheet(),
isScrollControlled: true,
backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
);
},
),
],
),
),
);
}
@ -308,6 +320,7 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
child: TabBarView(
controller: controller.tabController,
physics: const NeverScrollableScrollPhysics(),
clipBehavior: Clip.none,
children: [
Obx(
() => controller.isLoading.value
@ -350,199 +363,208 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
}
Widget _meetingWait(String title) {
return Stack(
alignment: Alignment.topCenter,
children: [
Lottie.asset(
'assets/lottie/meeting_wait.json',
),
Positioned(
top: 320.w,
child: Text(
title,
style: TextStyle(
fontSize: 18.sp,
color: Colors.black87,
fontWeight: FontWeight.w500,
return Builder(
builder: (context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Stack(
alignment: Alignment.topCenter,
children: [
Lottie.asset(
'assets/lottie/meeting_wait.json',
),
),
),
],
Positioned(
top: 320.w,
child: Text(
title,
style: TextStyle(
fontSize: 18.sp,
color: isDarkMode ? Colors.white : Colors.black87,
fontWeight: FontWeight.w500,
),
),
),
],
);
},
);
}
Widget _soundWave(bool isDarkMode) {
return Obx(
() => SizedBox(
height: controller.dragOffset.value < 0
? 0
: controller.dragOffset.value > 210.w
? 210.w
: controller.dragOffset.value,
child: Stack(
children: [
Positioned(
bottom: 0,
child: Container(
width: 1.sw,
height: 200.w,
margin: EdgeInsets.only(bottom: 10.w),
decoration: BoxDecoration(
color: Colors.grey[200]!,
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(20.r),
() => Opacity(
opacity: (controller.dragOffset.value / 210.w).clamp(0.0, 1.0),
child: SizedBox(
height: controller.dragOffset.value < 0
? 0
: controller.dragOffset.value > 210.w
? 210.w
: controller.dragOffset.value,
child: Stack(
children: [
Positioned(
bottom: 0,
child: Container(
width: 1.sw,
height: 200.w,
margin: EdgeInsets.only(bottom: 10.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900]! : Colors.grey[200]!,
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(20.r),
),
),
),
child: Column(
children: [
Container(
margin: EdgeInsets.only(bottom: 5.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8.r),
),
child: AudioFileWaveforms(
size: Size(350.w, 120.w),
playerController: controller.playerController,
waveformData: List.generate(88, (int index) {
return 0.01 + Random().nextDouble() * 0.09;
}),
waveformType: WaveformType.fitWidth,
playerWaveStyle: PlayerWaveStyle(
spacing: 4.w,
waveThickness: 2.w,
fixedWaveColor: Colors.grey[300]!,
liveWaveColor: Colors.red,
seekLineColor: Colors.red,
scaleFactor: 600,
child: Column(
children: [
Container(
margin: EdgeInsets.only(bottom: 5.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.white,
borderRadius: BorderRadius.circular(8.r),
),
child: AudioFileWaveforms(
size: Size(350.w, 120.w),
playerController: controller.playerController,
waveformData: List.generate(88, (int index) {
return 0.01 + Random().nextDouble() * 0.09;
}),
waveformType: WaveformType.fitWidth,
playerWaveStyle: PlayerWaveStyle(
spacing: 4.w,
waveThickness: 2.w,
fixedWaveColor: Colors.grey[300]!,
liveWaveColor: Colors.red,
seekLineColor: Colors.red,
scaleFactor: 600,
),
),
),
),
SizedBox(
width: 350.w,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Obx(
() => Text(
DateUtil.formatDateMs(
controller.currentDuration.value,
format: 'HH:mm:ss',
isUtc: true,
),
style: TextStyle(
color: isDarkMode
? Colors.grey[400]
: Colors.grey[500],
fontSize: 12.sp,
SizedBox(
width: 350.w,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Obx(
() => Text(
DateUtil.formatDateMs(
controller.currentDuration.value,
format: 'HH:mm:ss',
isUtc: true,
),
style: TextStyle(
color: isDarkMode
? Colors.grey[400]
: Colors.grey[500],
fontSize: 12.sp,
),
),
),
),
Obx(
() => Text(
DateUtil.formatDateMs(
controller.durationMax.value,
format: 'HH:mm:ss',
isUtc: true,
),
style: TextStyle(
color: isDarkMode
? Colors.grey[400]
: Colors.grey[500],
fontSize: 12.sp,
Obx(
() => Text(
DateUtil.formatDateMs(
controller.durationMax.value,
format: 'HH:mm:ss',
isUtc: true,
),
style: TextStyle(
color: isDarkMode
? Colors.grey[400]
: Colors.grey[500],
fontSize: 12.sp,
),
),
),
),
],
],
),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
GestureDetector(
onTap: () {
int progress =
controller.currentDuration.value - 10000;
if (progress < 0) {
progress = 0;
}
controller.playerController.seekTo(progress);
},
child: Icon(
Icons.replay_10,
color: Colors.black,
size: 30.w,
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
GestureDetector(
onTap: () {
int progress =
controller.currentDuration.value - 10000;
if (progress < 0) {
progress = 0;
}
controller.playerController.seekTo(progress);
},
child: Icon(
Icons.replay_10,
color: Colors.black,
size: 30.w,
),
),
),
GestureDetector(
onTap: () {
if (controller.meetingData.value.filepath.isEmpty) {
controller.downloadAudio();
} else {
if (controller.isPlay.value) {
controller.playerController.pausePlayer();
GestureDetector(
onTap: () {
if (controller
.meetingData.value.filepath.isEmpty) {
controller.downloadAudio();
} else {
controller.playerController.startPlayer();
if (controller.isPlay.value) {
controller.playerController.pausePlayer();
} else {
controller.playerController.startPlayer();
}
}
}
},
child: Obx(
() => !controller.isLoading.value &&
controller
.meetingData.value.filepath.isEmpty
? Obx(
() => Stack(
alignment: Alignment.center,
children: [
if (controller.isDownload.value)
SizedBox(
width: 42.w,
height: 42.w,
child: CircularProgressIndicator(
value: controller
.downloadProgress.value,
valueColor:
const AlwaysStoppedAnimation<
Color>(Colors.red),
strokeWidth: 3.w,
},
child: Obx(
() => !controller.isLoading.value &&
controller
.meetingData.value.filepath.isEmpty
? Obx(
() => Stack(
alignment: Alignment.center,
children: [
if (controller.isDownload.value)
SizedBox(
width: 42.w,
height: 42.w,
child: CircularProgressIndicator(
value: controller
.downloadProgress.value,
valueColor:
const AlwaysStoppedAnimation<
Color>(Colors.red),
strokeWidth: 3.w,
),
),
Icon(
Icons.download_for_offline,
color: Colors.black,
size: 48.w,
),
Icon(
Icons.download_for_offline,
color: Colors.black,
size: 48.w,
),
],
],
),
)
: Icon(
controller.isPlay.value
? Icons.pause_circle
: Icons.play_circle,
color: Colors.black,
size: 48.w,
),
)
: Icon(
controller.isPlay.value
? Icons.pause_circle
: Icons.play_circle,
color: Colors.black,
size: 48.w,
),
),
),
),
GestureDetector(
onTap: () {
int progress =
controller.currentDuration.value + 10000;
controller.playerController.seekTo(progress);
},
child: Icon(
Icons.forward_10,
color: Colors.black,
size: 30.w,
GestureDetector(
onTap: () {
int progress =
controller.currentDuration.value + 10000;
controller.playerController.seekTo(progress);
},
child: Icon(
Icons.forward_10,
color: Colors.black,
size: 30.w,
),
),
),
],
),
],
],
),
],
),
),
),
),
],
],
),
),
),
);

474
lib/modules/meeting/views/meeting_home_view.dart

@ -0,0 +1,474 @@
import 'package:common_utils/common_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:haptic_feedback/haptic_feedback.dart';
import '../../../core/theme/app_colors.dart';
import '../../../routes/app_routes.dart';
import '../controllers/meeting_home_controller.dart';
import '../model/meeting_model.dart';
import '../utils/meeting_ui_utils.dart';
import 'bottomSheet/edit_title_bottom_sheet.dart';
import 'bottomSheet/import_audio_bottom_sheet.dart';
class MeetingHomeView extends StatefulWidget {
const MeetingHomeView({super.key});
@override
State<MeetingHomeView> createState() => _MeetingHomeViewState();
}
class _MeetingHomeViewState extends State<MeetingHomeView>
with AutomaticKeepAliveClientMixin {
final _controller = Get.put(MeetingHomeController());
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return PopScope(
onPopInvokedWithResult: _controller.onPopInvokedWithResult,
child: Scaffold(
backgroundColor: MeetingUIUtils.getBackgroundColor(isDarkMode),
appBar: _buildAppBar(isDarkMode),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildFiltrate(isDarkMode),
_buildConnect(isDarkMode),
],
),
Expanded(
child: _buildBody(context, isDarkMode),
),
],
),
floatingActionButton: FloatingActionButton(
heroTag: "addAudio",
onPressed: () {
Get.bottomSheet(
const ImportAudioBottomSheet(),
isScrollControlled: true,
backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
);
},
child: const Icon(Icons.add),
),
),
);
}
Widget _buildFiltrate(bool isDarkMode) {
return GestureDetector(
onTap: () {
_controller.meetingController.scaffoldKey.currentState?.openDrawer();
},
behavior: HitTestBehavior.translucent,
child: Row(
children: [
12.horizontalSpace,
Container(
width: 32.w,
height: 32.w,
margin: EdgeInsets.symmetric(
vertical: 10.w,
),
decoration: BoxDecoration(
color: isDarkMode
? MeetingUIUtils.getTranslucentWhite(0.05)
: MeetingUIUtils.getCardColor(isDarkMode),
borderRadius: BorderRadius.circular(12.w),
boxShadow: MeetingUIUtils.getCardShadow(isDarkMode),
),
child: Icon(
Icons.format_list_bulleted,
color: isDarkMode ? Colors.white : Colors.black87,
size: 20.sp,
),
),
5.horizontalSpace,
Obx(
() => Text(
'${_controller.filtrateType.value.isNotEmpty ? _controller.filtrateType : '全部文件'}(${_controller.dataList.length})',
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
fontSize: 16.sp,
fontWeight: FontWeight.bold,
),
),
),
],
),
);
}
Widget _buildConnect(bool isDarkMode) {
return GestureDetector(
onTap: () {
Get.toNamed(Routes.meetingConnect);
},
child: Container(
padding: EdgeInsets.all(5.w),
margin: EdgeInsets.symmetric(
horizontal: 12.w,
vertical: 10.w,
),
decoration: BoxDecoration(
color: isDarkMode
? MeetingUIUtils.getTranslucentWhite(0.05)
: MeetingUIUtils.getCardColor(isDarkMode),
borderRadius: BorderRadius.circular(50.w),
boxShadow: MeetingUIUtils.getCardShadow(isDarkMode),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 32.w,
height: 32.w,
decoration: BoxDecoration(
color: MeetingUIUtils.getButtonColor(isDarkMode),
borderRadius: BorderRadius.circular(50.w),
),
child: Icon(
Icons.add,
color: Colors.white,
size: 20.sp,
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 5.w),
child: Text(
'连接',
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
fontSize: 14.sp,
),
),
)
],
),
),
);
}
// 构建应用栏
PreferredSizeWidget _buildAppBar(bool isDarkMode) {
return AppBar(
backgroundColor: isDarkMode ? Colors.black : Colors.white,
surfaceTintColor: Colors.transparent,
elevation: 0.5,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: isDarkMode ? Colors.white : Colors.black,
size: 20.sp,
),
onPressed: () => Get.back(),
),
title: Text(
'conferenceAssistant'.tr, // 商务会议助手
style: TextStyle(
fontSize: 17.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
centerTitle: true,
);
}
// 构建主体内容
Widget _buildBody(BuildContext context, bool isDarkMode) {
return Obx(
() => ListView.builder(
itemCount: _controller.dataList.length,
padding: EdgeInsets.fromLTRB(12.w, 5.w, 12.w, 90.w),
itemBuilder: (BuildContext context, int index) {
MeetingModel data =
MeetingModel.fromJson(_controller.dataList[index]);
return _item(context, isDarkMode, data);
},
),
);
}
Widget _item(BuildContext context, bool isDarkMode, MeetingModel data) {
return GestureDetector(
onTap: () {
Get.toNamed(Routes.meetingDetails, arguments: {
'id': data.id,
});
},
onLongPressStart: (LongPressStartDetails details) {
_onLongPressItem(context, isDarkMode, details, data);
},
child: Container(
padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 10.w),
margin: EdgeInsets.symmetric(vertical: 5.w),
decoration: BoxDecoration(
color: isDarkMode
? MeetingUIUtils.getTranslucentWhite(0.05)
: MeetingUIUtils.getCardColor(isDarkMode),
borderRadius: BorderRadius.circular(12.w),
boxShadow: MeetingUIUtils.getCardShadow(isDarkMode),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
data.title,
style: TextStyle(
color: MeetingUIUtils.getTextColor(isDarkMode),
fontSize: 18.sp,
),
overflow: TextOverflow.ellipsis,
),
Padding(
padding: EdgeInsets.symmetric(vertical: 5.w),
child: Row(
children: [
Icon(
Icons.calendar_month,
size: 14.w,
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
),
2.horizontalSpace,
Text(
DateUtil.formatDateMs(
data.creationtime,
format: DateFormats.y_mo_d_h_m,
),
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 12.sp,
),
),
20.horizontalSpace,
Icon(
Icons.access_time,
size: 14.w,
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
),
2.horizontalSpace,
Text(
_formatDateSeconds(data.seconds),
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 12.sp,
),
),
],
),
),
Row(
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 2.w),
margin: EdgeInsets.only(top: 5.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.grey[100],
borderRadius: BorderRadius.circular(4.w),
),
child: Text(
data.type,
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
fontSize: 10.sp,
),
),
),
const Spacer(),
Obx(() {
Map uploadData =
_controller.meetingUploadService.uploadList.firstWhere(
(element) => element['id'] == data.id,
orElse: () => {},
);
return uploadData.isNotEmpty
? Stack(
alignment: Alignment.center,
children: [
if (uploadData['id'] ==
_controller
.meetingUploadService.meetingId.value)
Obx(
() => SizedBox(
width: 20.w,
height: 20.w,
child: CircularProgressIndicator(
value: _controller.meetingUploadService
.uploadProgress.value,
valueColor:
const AlwaysStoppedAnimation<Color>(
Colors.red),
strokeWidth: 1.w,
),
),
),
SizedBox(
width: 20.w,
height: 20.w,
child: Icon(
uploadData['audiourl'].isNotEmpty
? Icons.cloud_done
: Icons.cloud_upload,
color: Colors.blue,
size: 16.w,
),
),
],
)
: const SizedBox();
}),
Obx(() {
Map taskData =
_controller.meetingTaskService.taskFinishList.firstWhere(
(element) => element['id'] == data.id,
orElse: () => {},
);
return taskData.isNotEmpty
? Padding(
padding: EdgeInsets.only(top: 5.w),
child: Text(
taskData['tasktype'] == 1
? '正在转写...'
: taskData['tasktype'] == 2
? '正在总结...'
: '总结完成',
style: TextStyle(
color: Colors.blue,
fontSize: 10.sp,
),
),
)
: const SizedBox();
}),
],
),
],
),
),
);
}
void _onLongPressItem(BuildContext context, bool isDarkMode,
LongPressStartDetails details, MeetingModel data) {
Haptics.vibrate(HapticsType.success);
RenderBox? renderBox =
Overlay.of(context).context.findRenderObject() as RenderBox;
final RelativeRect position = RelativeRect.fromRect(
Rect.fromLTRB(
details.globalPosition.dx,
details.globalPosition.dy,
details.globalPosition.dx + 110, // 菜单显示位置X轴坐标
details.globalPosition.dy - 40, // 菜单显示位置Y轴坐标
),
Offset.zero & renderBox.size,
);
showMenu(
context: context,
position: position,
color: isDarkMode ? AppColors.cardBackground : Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
items: [
_popupMenuEntry(
Icon(
Icons.edit_calendar,
size: 20.w,
color: isDarkMode ? Colors.white : Colors.black87,
),
'edit'.tr, // 编辑
isDarkMode ? Colors.white : Colors.black87,
() {
final dotIndex = data.title.lastIndexOf('.');
_controller.titleController.text =
dotIndex != -1 ? data.title.substring(0, dotIndex) : data.title;
_controller.titleCSuffix =
data.title.substring(dotIndex, data.title.length);
Get.bottomSheet(
EditTitleBottomSheet(data.id),
isScrollControlled: true,
backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
);
},
),
_popupMenuEntry(
Icon(
Icons.delete_outline,
size: 20.w,
color: isDarkMode ? Colors.red[700] : Colors.red,
),
'delete'.tr, // 删除
isDarkMode ? Colors.red[700]! : Colors.red,
() {
_controller.deleteMeeting(data.id, data.filepath);
},
),
],
);
}
PopupMenuEntry _popupMenuEntry(
Icon icon,
String title,
Color color,
Function() onTap,
) {
return PopupMenuItem(
onTap: onTap,
child: SizedBox(
width: 140.w,
child: Row(
children: [
icon,
8.horizontalSpace,
Text(
title,
style: TextStyle(
color: color,
fontSize: 14.sp,
),
),
],
),
),
);
}
String _formatDateSeconds(int seconds) {
if (seconds < 60) {
return '${seconds}s';
} else if (seconds < 3600) {
int minutes = seconds ~/ 60;
int remainingSeconds = seconds % 60;
return '${minutes}m ${remainingSeconds}s';
} else {
int hours = seconds ~/ 3600;
int remainingMinutes = (seconds % 3600) ~/ 60;
int remainingSeconds = seconds % 60;
return '${hours}h ${remainingMinutes}m ${remainingSeconds}s';
}
}
}

439
lib/modules/meeting/views/meeting_mine_view.dart

@ -0,0 +1,439 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../../../routes/app_routes.dart';
import '../../profile/views/unified_avatar.dart';
import '../controllers/meeting_mine_controller.dart';
import '../utils/meeting_ui_utils.dart';
import 'bottomSheet/statistics_bottom_sheet.dart';
class MeetingMineView extends StatefulWidget {
const MeetingMineView({super.key});
@override
State<MeetingMineView> createState() => _MeetingMineViewState();
}
class _MeetingMineViewState extends State<MeetingMineView>
with AutomaticKeepAliveClientMixin {
final _controller = Get.put(MeetingMineController());
@override
bool get wantKeepAlive => true;
// 添加 isDarkMode 作为类的 getter
bool get isDarkMode => Theme.of(context).brightness == Brightness.dark;
@override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
backgroundColor: MeetingUIUtils.getBackgroundColor(isDarkMode),
body: Container(
decoration: const BoxDecoration(
image: DecorationImage(
alignment: Alignment.topCenter,
image: AssetImage('assets/images/mine-background.png'),
fit: BoxFit.fitWidth,
),
),
child: SafeArea(
child: ListView(
padding: EdgeInsets.symmetric(horizontal: 12.w),
children: [
_head(),
_statistics(),
_member(),
_box([
_item(
Icons.article_outlined,
'转写记录',
onTap: () {},
),
]),
_box([
_item(
Icons.settings_outlined,
'设置',
onTap: () {},
),
_item(
Icons.layers_outlined,
'模版社区',
onTap: () {},
),
_item(
Icons.sync_alt_outlined,
'集成',
onTap: () {},
),
]),
_box([
_item(
Icons.cloud_upload_outlined,
'DEAPSOUND PRIVATE CLOUD',
description: '开启',
onTap: () {},
),
]),
_box([
_item(
Icons.language_outlined,
'WEB',
description: 'web.deapsound.com',
onTap: () {},
),
_item(
Icons.settings_remote_outlined,
'我的DEAPSOUND设备',
onTap: () {},
),
]),
_box([
_item(
Icons.help_outline_rounded,
'帮助与反馈',
onTap: () {},
),
_item(
Icons.info_outline_rounded,
'关于DEAPSOUND',
description: 'v1.0.0',
onTap: () {},
),
]),
],
),
),
),
);
}
Widget _head() {
return Padding(
padding: EdgeInsets.symmetric(vertical: 20.w),
child: GestureDetector(
onTap: () => Get.toNamed(Routes.profileMeeting), // 跳转到个人信息页面
child: Row(
children: [
// 使用统一头像组件,避免闪烁问题
Container(
margin: EdgeInsets.only(right: 10.w),
child: UnifiedAvatar(
radius: 25.w,
backgroundColor: Colors.grey[200],
iconColor: Colors.grey[600],
iconSize: 25.r,
isDarkMode: isDarkMode,
),
),
Expanded(
child: Obx(
() => Text(
_controller.profileCtrl.userInfo.value.name,
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
fontSize: 20.sp,
fontWeight: FontWeight.bold,
),
),
),
),
Icon(
Icons.arrow_forward_ios,
size: 16.w,
color: Colors.grey[700],
),
],
),
),
);
}
Widget _statistics() {
return GestureDetector(
onTap: () {
Get.bottomSheet(
const StatisticsBottomSheet(),
isScrollControlled: true,
);
},
child: Container(
padding: EdgeInsets.all(8.w),
margin: EdgeInsets.only(bottom: 15.w),
decoration: BoxDecoration(
color: MeetingUIUtils.getCardColor(isDarkMode),
borderRadius: BorderRadius.circular(16.w),
boxShadow: MeetingUIUtils.getCardShadow(isDarkMode),
),
child: Obx(
() => Column(
children: [
Row(
children: [
_statisticsItem(
'${_controller.days.value}',
'天',
Colors.lime,
),
_statisticsItem(
'${_controller.numberFiles.value}',
'文件',
Colors.lightGreen,
),
_statisticsItem(
'${_controller.hourage.value}',
'小时',
Colors.blueGrey,
),
],
),
Container(
height: 80.w,
padding: EdgeInsets.symmetric(horizontal: 2.w),
margin: EdgeInsets.only(top: 8.w, bottom: 2.w),
child: GridView.builder(
controller: _controller.scrollController,
scrollDirection: Axis.horizontal,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 7,
mainAxisSpacing: 2.w,
crossAxisSpacing: 2.w,
),
itemCount: 364,
itemBuilder: (BuildContext context, int index) {
int type = 0;
if (_controller.statistics.containsKey(index + 1)) {
if (_controller.statistics[index + 1]['count'] <=
_controller.averageNumberFiles) {
type = 1;
} else {
type = 2;
}
}
return Container(
decoration: BoxDecoration(
color: type == 0
? Colors.grey[200]
: type == 1
? Colors.red[100]
: Colors.red,
borderRadius: BorderRadius.circular(2.w),
),
);
},
),
),
],
),
),
),
);
}
Widget _statisticsItem(String amount, String unit, Color color) {
return Expanded(
child: Container(
height: 30.w,
margin: EdgeInsets.symmetric(horizontal: 2.w),
alignment: Alignment.center,
decoration: BoxDecoration(
color: color.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(8.w),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
amount,
style: TextStyle(
height: 0.1,
color: isDarkMode ? Colors.white : Colors.black87,
fontSize: 16.sp,
fontWeight: FontWeight.bold,
),
),
2.horizontalSpace,
Text(
unit,
style: TextStyle(
height: 0.1,
color: Colors.grey,
fontSize: 10.sp,
),
),
],
),
),
);
}
Widget _member() {
return Container(
margin: EdgeInsets.only(bottom: 15.w),
decoration: BoxDecoration(
color: MeetingUIUtils.getCardColor(isDarkMode),
borderRadius: BorderRadius.circular(16.w),
),
child: Column(
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'DEAPSOUND AI会员',
style: TextStyle(
color: MeetingUIUtils.getTextColor(isDarkMode),
fontSize: 16.sp,
),
),
Icon(
Icons.arrow_forward_ios,
size: 16.w,
color: Colors.grey,
),
],
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 15.w),
decoration: BoxDecoration(
color: Colors.black87,
borderRadius: BorderRadius.circular(16.w),
),
child: Column(
children: [
Row(
children: [
Icon(
Icons.auto_awesome,
color: Colors.amberAccent,
size: 25.sp,
),
5.horizontalSpace,
Text(
'解锁 Starter 会员',
style: TextStyle(
color: Colors.white,
fontSize: 18.sp,
fontWeight: FontWeight.bold,
),
),
],
),
Padding(
padding: EdgeInsets.only(top: 15.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'绑定 DEAPSOUND 设备可激活 Starter 会员',
style: TextStyle(
color: Colors.white,
fontSize: 12.sp,
),
),
GestureDetector(
onTap: () {},
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 10.w,
vertical: 6.w,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(50.w),
gradient: const LinearGradient(
colors: [Colors.blue, Colors.pink],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
),
child: Text(
'立即解锁',
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
],
),
),
],
),
);
}
Widget _box(List<Widget> children) {
return Container(
padding: EdgeInsets.symmetric(vertical: 5.w),
margin: EdgeInsets.only(bottom: 15.w),
decoration: BoxDecoration(
color: MeetingUIUtils.getCardColor(isDarkMode),
borderRadius: BorderRadius.circular(16.w),
boxShadow: MeetingUIUtils.getCardShadow(isDarkMode),
),
child: Column(
children: children,
),
);
}
Widget _item(
IconData icon,
String title, {
String? description,
Function()? onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 12.w),
child: Row(
children: [
Icon(
icon,
size: 24.w,
color: isDarkMode ? Colors.grey[400] : Colors.grey,
),
8.horizontalSpace,
Expanded(
child: Text(
title,
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
fontSize: 16.sp,
),
),
),
if (description != null)
Padding(
padding: EdgeInsets.only(right: 5.w),
child: Text(
description,
style: TextStyle(
color: Colors.grey,
fontSize: 16.sp,
),
),
),
Icon(
Icons.arrow_forward_ios,
size: 16.w,
color: isDarkMode ? Colors.grey[400] : Colors.grey,
),
],
),
),
);
}
}

506
lib/modules/meeting/views/meeting_view.dart

@ -1,444 +1,120 @@
import 'package:common_utils/common_utils.dart';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:haptic_feedback/haptic_feedback.dart';
import '../../../core/theme/app_colors.dart';
import '../../../routes/app_routes.dart';
import '../controllers/meeting_controller.dart';
import '../model/meeting_model.dart';
import 'bottomSheet/edit_title_bottom_sheet.dart';
import '../utils/meeting_ui_utils.dart';
import 'bottomSheet/filtrate_drawer.dart';
import 'bottomSheet/import_audio_bottom_sheet.dart';
import 'bottomSheet/navigation_bar_bottom_sheet.dart';
class MeetingView extends GetView<MeetingController> {
const MeetingView({super.key});
@override
Widget build(BuildContext context) {
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return PopScope(
onPopInvokedWithResult: controller.onPopInvokedWithResult,
child: Scaffold(
key: scaffoldKey,
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
appBar: _buildAppBar(isDarkMode),
drawer: Drawer(
width: 260.w,
child: const FiltrateDrawer(),
),
drawerEnableOpenDragGesture: false,
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: Icon(
Icons.folder_open,
color: isDarkMode ? Colors.white : Colors.black,
),
onPressed: () {
scaffoldKey.currentState?.openDrawer();
},
return Scaffold(
key: controller.scaffoldKey,
backgroundColor: MeetingUIUtils.getBackgroundColor(isDarkMode),
body: PageView(
physics: const NeverScrollableScrollPhysics(),
controller: controller.pageController,
children: controller.pageList,
),
drawer: Drawer(
width: 260.w,
backgroundColor: isDarkMode ? const Color(0xFF1A1A1A) : Colors.grey[50],
child: const FiltrateDrawer(),
),
drawerEnableOpenDragGesture: false,
bottomNavigationBar: Stack(
alignment: Alignment.center,
clipBehavior: Clip.none,
children: [
Obx(
() => BottomNavigationBar(
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.folder_outlined),
activeIcon: Icon(Icons.folder),
label: '文件',
),
_buildConnect(isDarkMode),
],
),
Expanded(
child: _buildBody(context, isDarkMode),
),
],
),
floatingActionButton: FloatingActionButton(
heroTag: "addAudio",
onPressed: () {
Get.bottomSheet(
const ImportAudioBottomSheet(),
isScrollControlled: true,
backgroundColor: Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
BottomNavigationBarItem(
icon: SizedBox(),
label: '',
),
),
);
},
child: const Icon(Icons.add),
),
),
);
}
Widget _buildConnect(bool isDarkMode) {
return GestureDetector(
onTap: () {
Get.toNamed(Routes.meetingConnect);
},
child: Container(
padding: EdgeInsets.all(5.w),
margin: EdgeInsets.symmetric(
horizontal: 12.w,
vertical: 10.w,
),
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white,
borderRadius: BorderRadius.circular(50.w),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 32.w,
height: 32.w,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(50.w),
),
child: Icon(
Icons.add,
color: Colors.white,
size: 20.sp,
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 5.w),
child: Text(
'连接',
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
fontSize: 14.sp,
BottomNavigationBarItem(
icon: Icon(Icons.person_outline),
activeIcon: Icon(Icons.person),
label: '我的',
),
),
)
],
),
),
);
}
// 构建应用栏
PreferredSizeWidget _buildAppBar(bool isDarkMode) {
return AppBar(
backgroundColor: isDarkMode ? Colors.black : Colors.white,
surfaceTintColor: Colors.transparent,
elevation: 0.5,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: isDarkMode ? Colors.white : Colors.black,
size: 20.sp,
),
onPressed: () => Get.back(),
),
title: Text(
'conferenceAssistant'.tr, // 商务会议助手
style: TextStyle(
fontSize: 17.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
centerTitle: true,
);
}
// 构建主体内容
Widget _buildBody(BuildContext context, bool isDarkMode) {
return Obx(
() => ListView.builder(
controller: controller.listController,
itemCount: controller.dataList.length,
padding: EdgeInsets.fromLTRB(12.w, 5.w, 12.w, 90.w),
itemBuilder: (BuildContext context, int index) {
MeetingModel data = MeetingModel.fromJson(controller.dataList[index]);
return _item(context, isDarkMode, data);
},
),
);
}
Widget _item(BuildContext context, bool isDarkMode, MeetingModel data) {
return GestureDetector(
onTap: () {
Get.toNamed(Routes.meetingDetails, arguments: {
'id': data.id,
});
},
onLongPressStart: (LongPressStartDetails details) {
_onLongPressItem(context, isDarkMode, details, data);
},
child: Container(
padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 10.w),
margin: EdgeInsets.symmetric(vertical: 5.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white,
borderRadius: BorderRadius.circular(12.w),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
data.title,
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
fontSize: 18.sp,
),
overflow: TextOverflow.ellipsis,
],
backgroundColor: isDarkMode ? const Color(0xFF1A1A1A) : Colors.white,
currentIndex: controller.pageIndex.value,
unselectedItemColor: Colors.grey,
selectedItemColor: isDarkMode ? Colors.white : Colors.blue,
unselectedFontSize: 10.sp,
selectedFontSize: 10.sp,
onTap: (int index) {
if (index == 1 || index == controller.pageIndex.value) return;
if (index == 0) {
controller.pageIndex.value = 0;
controller.pageController.jumpToPage(0);
} else {
controller.pageIndex.value = 2;
controller.pageController.jumpToPage(1);
}
},
),
Padding(
padding: EdgeInsets.symmetric(vertical: 5.w),
child: Row(
children: [
Icon(
Icons.calendar_month,
size: 14.w,
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
),
2.horizontalSpace,
Text(
DateUtil.formatDateMs(
data.creationtime,
format: DateFormats.y_mo_d_h_m,
),
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 12.sp,
),
),
20.horizontalSpace,
Icon(
Icons.access_time,
size: 14.w,
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
),
Positioned(
top: -18.w,
child: GestureDetector(
onTap: () {
Get.bottomSheet(
BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
child: const NavigationBarBottomSheet(),
),
2.horizontalSpace,
Text(
_formatDateSeconds(data.seconds),
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 12.sp,
isScrollControlled: true,
);
},
child: Container(
width: 60.w,
height: 60.w,
padding: EdgeInsets.all(5.w),
alignment: Alignment.center,
decoration: BoxDecoration(
color: isDarkMode ? const Color(0xFF1A1A1A) : Colors.white,
borderRadius: BorderRadius.circular(60.w),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: isDarkMode ? 0.3 : 0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
),
],
),
),
Row(
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 2.w),
margin: EdgeInsets.only(top: 5.w),
],
),
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.grey[100],
borderRadius: BorderRadius.circular(4.w),
color: MeetingUIUtils.getButtonColor(isDarkMode),
borderRadius: BorderRadius.circular(60.w),
),
child: Text(
data.type,
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
fontSize: 10.sp,
),
child: Icon(
Icons.add,
color: Colors.white,
size: 30.w,
),
),
const Spacer(),
Obx(() {
Map uploadData =
controller.meetingUploadService.uploadList.firstWhere(
(element) => element['id'] == data.id,
orElse: () => {},
);
return uploadData.isNotEmpty
? Stack(
alignment: Alignment.center,
children: [
if (uploadData['id'] ==
controller.meetingUploadService.meetingId.value)
Obx(
() => SizedBox(
width: 20.w,
height: 20.w,
child: CircularProgressIndicator(
value: controller.meetingUploadService
.uploadProgress.value,
valueColor:
const AlwaysStoppedAnimation<Color>(
Colors.red),
strokeWidth: 1.w,
),
),
),
SizedBox(
width: 20.w,
height: 20.w,
child: Icon(
uploadData['audiourl'].isNotEmpty
? Icons.cloud_done
: Icons.cloud_upload,
color: Colors.blue,
size: 16.w,
),
),
],
)
: const SizedBox();
}),
Obx(() {
Map taskData =
controller.meetingTaskService.taskFinishList.firstWhere(
(element) => element['id'] == data.id,
orElse: () => {},
);
return taskData.isNotEmpty
? Padding(
padding: EdgeInsets.only(top: 5.w),
child: Text(
taskData['tasktype'] == 1
? '正在转写...'
: taskData['tasktype'] == 2
? '正在总结...'
: '总结完成',
style: TextStyle(
color: Colors.blue,
fontSize: 10.sp,
),
),
)
: const SizedBox();
}),
],
),
],
),
),
);
}
void _onLongPressItem(BuildContext context, bool isDarkMode,
LongPressStartDetails details, MeetingModel data) {
Haptics.vibrate(HapticsType.success);
RenderBox? renderBox =
Overlay.of(context).context.findRenderObject() as RenderBox;
final RelativeRect position = RelativeRect.fromRect(
Rect.fromLTRB(
details.globalPosition.dx,
details.globalPosition.dy,
details.globalPosition.dx + 110, // 菜单显示位置X轴坐标
details.globalPosition.dy - 40, // 菜单显示位置Y轴坐标
),
Offset.zero & renderBox.size,
);
showMenu(
context: context,
position: position,
color: isDarkMode ? AppColors.cardBackground : Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
items: [
_popupMenuEntry(
Icon(
Icons.edit_calendar,
size: 20.w,
color: isDarkMode ? Colors.white : Colors.black87,
),
'edit'.tr, // 编辑
isDarkMode ? Colors.white : Colors.black87,
() {
final dotIndex = data.title.lastIndexOf('.');
controller.titleController.text =
dotIndex != -1 ? data.title.substring(0, dotIndex) : data.title;
controller.titleCSuffix =
data.title.substring(dotIndex, data.title.length);
Get.bottomSheet(
EditTitleBottomSheet(data.id),
isScrollControlled: true,
backgroundColor: Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
);
},
),
_popupMenuEntry(
Icon(
Icons.delete_outline,
size: 20.w,
color: isDarkMode ? Colors.red[700] : Colors.red,
),
'delete'.tr, // 删除
isDarkMode ? Colors.red[700]! : Colors.red,
() {
controller.deleteMeeting(data.id, data.filepath);
},
),
],
);
}
PopupMenuEntry _popupMenuEntry(
Icon icon,
String title,
Color color,
Function() onTap,
) {
return PopupMenuItem(
onTap: onTap,
child: SizedBox(
width: 140.w,
child: Row(
children: [
icon,
8.horizontalSpace,
Text(
title,
style: TextStyle(
color: color,
fontSize: 14.sp,
),
),
],
),
),
],
),
);
}
String _formatDateSeconds(int seconds) {
if (seconds < 60) {
return '${seconds}s';
} else if (seconds < 3600) {
int minutes = seconds ~/ 60;
int remainingSeconds = seconds % 60;
return '${minutes}m ${remainingSeconds}s';
} else {
int hours = seconds ~/ 3600;
int remainingMinutes = (seconds % 3600) ~/ 60;
int remainingSeconds = seconds % 60;
return '${hours}h ${remainingMinutes}m ${remainingSeconds}s';
}
}
}

39
lib/modules/meeting/views/tabs/overview_tab.dart

@ -4,6 +4,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../../controllers/meeting_details_controller.dart';
import '../../utils/meeting_ui_utils.dart';
import '../bottomSheet/ai_bottom_sheet.dart';
import '../bottomSheet/generate_bottom_sheet.dart';
@ -29,6 +30,7 @@ class _OverviewTabState extends State<OverviewTab> {
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Stack(
children: [
_dataList.isNotEmpty
@ -43,7 +45,7 @@ class _OverviewTabState extends State<OverviewTab> {
format: DateFormats.y_mo_d_h_m,
),
style: TextStyle(
color: Colors.black54,
color: isDarkMode ? Colors.grey[400] : Colors.black54,
fontSize: 12.sp,
),
),
@ -55,6 +57,7 @@ class _OverviewTabState extends State<OverviewTab> {
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black,
),
),
),
@ -62,7 +65,7 @@ class _OverviewTabState extends State<OverviewTab> {
padding: EdgeInsets.symmetric(vertical: 10.w),
child: Divider(
height: 1,
color: Colors.grey[300],
color: isDarkMode ? Colors.grey[700] : Colors.grey[300],
),
),
Padding(
@ -70,7 +73,7 @@ class _OverviewTabState extends State<OverviewTab> {
child: Text(
'分析出${_dataList.length}个关键事件。',
style: TextStyle(
color: Colors.grey,
color: isDarkMode ? Colors.grey[400] : Colors.grey,
fontSize: 12.sp,
),
),
@ -103,7 +106,7 @@ class _OverviewTabState extends State<OverviewTab> {
Get.bottomSheet(
const AIBottomSheet(),
isScrollControlled: true,
backgroundColor: Colors.white,
backgroundColor: isDarkMode ? Colors.grey[800] : Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
@ -122,7 +125,7 @@ class _OverviewTabState extends State<OverviewTab> {
Get.bottomSheet(
const GenerateBottomSheet(),
isScrollControlled: true,
backgroundColor: Colors.grey[100],
backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
@ -141,22 +144,21 @@ class _OverviewTabState extends State<OverviewTab> {
}
Widget _item(String data) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Container(
padding: EdgeInsets.all(10.w),
margin: EdgeInsets.symmetric(vertical: 5.w),
decoration: BoxDecoration(
color: Colors.white,
color: MeetingUIUtils.getCardColor(isDarkMode),
borderRadius: BorderRadius.circular(12.w),
boxShadow: const [
BoxShadow(
color: Color(0x1A3045C8),
offset: Offset(0.0, 0.0),
blurRadius: 10,
spreadRadius: 0.0,
),
],
boxShadow: MeetingUIUtils.getCardShadow(isDarkMode),
),
child: Text(
data,
style: TextStyle(
color: MeetingUIUtils.getTextColor(isDarkMode),
),
),
child: Text(data),
);
}
@ -194,6 +196,7 @@ class _OverviewTabState extends State<OverviewTab> {
}
Widget _empty() {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 30.w),
child: Column(
@ -202,14 +205,14 @@ class _OverviewTabState extends State<OverviewTab> {
padding: EdgeInsets.only(bottom: 10.w),
child: Icon(
Icons.summarize,
color: Colors.grey,
color: isDarkMode ? Colors.grey[400] : Colors.grey,
size: 40.sp,
),
),
Text(
'概览',
style: TextStyle(
color: Colors.black87,
color: MeetingUIUtils.getTextColor(isDarkMode),
fontSize: 18.sp,
fontWeight: FontWeight.bold,
),
@ -217,7 +220,7 @@ class _OverviewTabState extends State<OverviewTab> {
Text(
'AI分析识别录音中的关键事件,让你选择重要事件进行AI生成。',
style: TextStyle(
color: Colors.grey,
color: MeetingUIUtils.getSecondaryTextColor(isDarkMode),
fontSize: 14.sp,
),
textAlign: TextAlign.center,

22
lib/modules/meeting/views/tabs/speech_tab.dart

@ -5,6 +5,7 @@ import 'package:get/get.dart';
import 'package:lottie/lottie.dart';
import '../../controllers/meeting_details_controller.dart';
import '../../utils/meeting_ui_utils.dart';
import '../bottomSheet/ai_bottom_sheet.dart';
import '../bottomSheet/generate_bottom_sheet.dart';
import '../bottomSheet/speaker_all_bottom_sheet.dart';
@ -15,6 +16,7 @@ class SpeechTab extends GetView<MeetingDetailsController> {
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Stack(
children: [
Obx(
@ -25,7 +27,7 @@ class SpeechTab extends GetView<MeetingDetailsController> {
Map item = controller.textList[index];
TextStyle textStyle = TextStyle(
fontSize: 15.sp,
color: Colors.black87,
color: MeetingUIUtils.getTextColor(isDarkMode),
letterSpacing: 0,
height: 1.4,
);
@ -56,7 +58,7 @@ class SpeechTab extends GetView<MeetingDetailsController> {
Get.bottomSheet(
SpeakerBottomSheet(item['id'], item['speaker']),
isScrollControlled: true,
backgroundColor: Colors.grey[100],
backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
@ -82,7 +84,7 @@ class SpeechTab extends GetView<MeetingDetailsController> {
isUtc: true,
),
style: TextStyle(
color: Colors.grey,
color: MeetingUIUtils.getSecondaryTextColor(isDarkMode),
fontSize: 13.sp,
),
),
@ -134,7 +136,6 @@ class SpeechTab extends GetView<MeetingDetailsController> {
() => Container(
padding: EdgeInsets.symmetric(
horizontal: 14.w,
vertical: 5.w,
),
decoration: BoxDecoration(
color:
@ -173,11 +174,12 @@ class SpeechTab extends GetView<MeetingDetailsController> {
style: textStyle,
minLines: 1,
maxLines: 100,
decoration: const InputDecoration(
decoration: InputDecoration(
isDense: true,
fillColor: Colors.transparent,
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
contentPadding:
EdgeInsets.symmetric(vertical: 5.w),
),
onChanged: (String value) {
controller.editSpeakerTextChange = value;
@ -227,7 +229,7 @@ class SpeechTab extends GetView<MeetingDetailsController> {
item['content'],
style: TextStyle(
fontSize: 15.sp,
color: isActive ? Colors.red : Colors.black87,
color: isActive ? Colors.red : MeetingUIUtils.getTextColor(isDarkMode),
letterSpacing: 0,
height: 1.4,
),
@ -270,7 +272,7 @@ class SpeechTab extends GetView<MeetingDetailsController> {
Get.bottomSheet(
const SpeakerAllBottomSheet(),
isScrollControlled: true,
backgroundColor: Colors.grey[100],
backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
@ -288,7 +290,7 @@ class SpeechTab extends GetView<MeetingDetailsController> {
Get.bottomSheet(
const AIBottomSheet(),
isScrollControlled: true,
backgroundColor: Colors.white,
backgroundColor: isDarkMode ? Colors.grey[800] : Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
@ -307,7 +309,7 @@ class SpeechTab extends GetView<MeetingDetailsController> {
Get.bottomSheet(
const GenerateBottomSheet(),
isScrollControlled: true,
backgroundColor: Colors.grey[100],
backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),

178
lib/modules/meeting/views/tabs/summary_tab.dart

@ -1,8 +1,9 @@
import 'package:common_utils/common_utils.dart';
import 'package:fleather/fleather.dart';
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:parchment/codecs.dart';
import '../../../../data/services/db/sqflite_api.dart';
import '../../controllers/meeting_details_controller.dart';
@ -19,6 +20,9 @@ class SummaryTab extends StatefulWidget {
class _SummaryTabState extends State<SummaryTab> {
final _controller = Get.find<MeetingDetailsController>();
late final Worker _personnelListener;
final String _addressPrefix = '地点:';
String _addressText = '';
final String _personnelPrefix = '与会人员:';
@ -26,9 +30,30 @@ class _SummaryTabState extends State<SummaryTab> {
final TextEditingController _addressController = TextEditingController();
final TextEditingController _personnelController = TextEditingController();
final FocusNode _focusNode = FocusNode();
final GlobalKey<EditorState> _editorKey = GlobalKey();
FleatherController? _fleatherController;
bool _hasFocus = false;
@override
void initState() {
super.initState();
_initController();
_addressController.addListener(_handleAddressChanged);
_personnelController.addListener(_handlePersonnelChanged);
_focusNode.addListener(() {
setState(() {
_hasFocus = _focusNode.hasFocus;
});
});
_personnelListener =
ever(_controller.personnelNumberModifications, (value) {
_initController();
});
}
void _initController() {
_addressController.text = _addressPrefix +
(_controller.meetingDetails.value.address.isNotEmpty
? _controller.meetingDetails.value.address
@ -37,8 +62,11 @@ class _SummaryTabState extends State<SummaryTab> {
(_controller.meetingDetails.value.personnel.isNotEmpty
? _controller.meetingDetails.value.personnel
: '[输入与会人员]');
_addressController.addListener(_handleAddressChanged);
_personnelController.addListener(_handlePersonnelChanged);
final markdown = const ParchmentMarkdownCodec()
.decode(_controller.meetingDetails.value.summary);
setState(() {
_fleatherController = FleatherController(document: markdown);
});
}
void _handleAddressChanged() {
@ -101,8 +129,25 @@ class _SummaryTabState extends State<SummaryTab> {
}
}
void _saveSummary() async {
final markdown =
const ParchmentMarkdownCodec().encode(_fleatherController!.document);
if (markdown != _controller.meetingDetails.value.summary) {
SqfliteApi.editMeeting(
_controller.id,
{'summary': markdown},
);
_controller.meetingDetails.value.summary = markdown;
_controller.summaryNumberModifications.value++;
}
}
@override
void dispose() {
_personnelListener.dispose();
_saveSummary();
_fleatherController?.dispose();
_focusNode.dispose();
_addressController.dispose();
_personnelController.dispose();
super.dispose();
@ -119,6 +164,7 @@ class _SummaryTabState extends State<SummaryTab> {
);
return Stack(
clipBehavior: Clip.none,
children: [
SingleChildScrollView(
padding: EdgeInsets.only(bottom: 80.w),
@ -202,7 +248,7 @@ class _SummaryTabState extends State<SummaryTab> {
left: BorderSide(
width: 2,
color: isDarkMode
? Colors.white.withOpacity(0.1)
? Colors.white.withValues(alpha: 0.1)
: Colors.grey[300]!,
),
),
@ -278,67 +324,51 @@ class _SummaryTabState extends State<SummaryTab> {
),
Padding(
padding: EdgeInsets.only(top: 10.w),
child: MarkdownBody(
data: _controller.meetingDetails.value.summary,
child: FleatherEditor(
controller: _fleatherController!,
focusNode: _focusNode,
editorKey: _editorKey,
),
),
],
),
),
),
Obx(
() => Positioned(
bottom: 20.w,
left: 0,
right: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_controller.meetingDetails.value.tasktype >= 3)
_button(
Icons.supervisor_account,
'编辑发言者',
() {
Get.bottomSheet(
const SpeakerAllBottomSheet(),
isScrollControlled: true,
backgroundColor: Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
);
},
),
if (_controller.meetingDetails.value.tasktype >= 3)
_button(
Icons.auto_awesome,
'Ask AI',
() {
Get.bottomSheet(
const AIBottomSheet(),
isScrollControlled: true,
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
if (!_hasFocus)
Obx(
() => Positioned(
bottom: 20.w,
left: 0,
right: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_controller.meetingDetails.value.tasktype >= 3)
_button(
Icons.supervisor_account,
'编辑发言者',
() {
Get.bottomSheet(
const SpeakerAllBottomSheet(),
isScrollControlled: true,
backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
),
);
},
),
if (_controller.meetingDetails.value.tasktype == 0)
SizedBox(
width: 150.w,
child: _button(
);
},
),
if (_controller.meetingDetails.value.tasktype >= 3)
_button(
Icons.auto_awesome,
'生成',
'Ask AI',
() {
Get.bottomSheet(
const GenerateBottomSheet(),
const AIBottomSheet(),
isScrollControlled: true,
backgroundColor: Colors.grey[100],
backgroundColor: isDarkMode ? Colors.grey[800] : Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
@ -347,11 +377,45 @@ class _SummaryTabState extends State<SummaryTab> {
);
},
),
),
],
if (_controller.meetingDetails.value.tasktype == 0)
SizedBox(
width: 150.w,
child: _button(
Icons.auto_awesome,
'生成',
() {
Get.bottomSheet(
const GenerateBottomSheet(),
isScrollControlled: true,
backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
);
},
),
),
],
),
),
),
if (_hasFocus)
Positioned(
bottom: 0,
left: -12.w,
right: -12.w,
child: Container(
height: 40,
color: isDarkMode ? Colors.grey[800] : Colors.white,
alignment: Alignment.center,
child: FleatherToolbar.basic(
controller: _fleatherController!,
editorKey: _editorKey,
),
),
),
),
],
);
}

2
lib/modules/meeting/views/template_all_view.dart → lib/modules/meeting/views/template/template_all_view.dart

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../../../data/services/db/sqflite_api.dart';
import '../../../../data/services/db/sqflite_api.dart';
class TemplateAllView extends StatefulWidget {
final String title;

0
lib/modules/meeting/views/template_view.dart → lib/modules/meeting/views/template/template_view.dart

273
lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart

@ -1,4 +1,6 @@
import 'package:classic_bluetooth/classic_bluetooth.dart';
import '/routes/app_routes.dart';
import 'package:logger/web.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:path/path.dart';
@ -121,7 +123,17 @@ class OtaUpgradeController extends GetxController {
// 实现更新逻辑...
} catch (e) {
errorMessage.value = e.toString();
Get.snackbar('更新失败', e.toString());
Get.snackbar(
'更新失败',
e.toString(),
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red[600],
colorText: Colors.white,
icon: const Icon(
Icons.error_outline,
color: Colors.white,
),
);
}
}
@ -166,7 +178,7 @@ class OtaUpgradeController extends GetxController {
// 从第三个 `.` 开始分割
final firstPart = firmwareParts.sublist(0, 3).join('.');
final secondPart = firmwareParts.sublist(3).join('.');
print("firstPart${firstPart},secondPart=${secondPart}");
Logger().d("firstPart${firstPart},secondPart=${secondPart}");
// 判断两部分是否一致
if (firstPart != secondPart) {
throw "固件版本格式错误:${deviceInfo.firmwareVersion}";
@ -174,19 +186,13 @@ class OtaUpgradeController extends GetxController {
// 正常流程
currentVersion.value = firstPart;
print('getequipmentinfo,id: ${deviceInfo.firmwareProductId}');
Logger().d('getequipmentinfo,id: ${deviceInfo.firmwareProductId}');
final connectedDevices = await ClassicBluetooth.getConnectedDevices();
if (connectedDevices.isNotEmpty) {
_address = connectedDevices.first.address;
}
await _bleManager.setEnterOta(true);
await Future.delayed(const Duration(seconds: 2));
jlota.init();
// API请求
final response = await Api.getproduct({
"id": deviceInfo.firmwareProductId,
@ -198,20 +204,35 @@ class OtaUpgradeController extends GetxController {
}
final resp = OtaConfigResp.fromJson(response);
print('getequipmentinfo,resp.systemversion: ${resp.systemversion}');
Logger().d('getequipmentinfo,resp.systemversion: ${resp.systemversion}');
latestVersion.value = resp.systemversion!;
updatedescription.value = resp.updatedescription!;
updatepackageaddress = resp.updatepackageaddress!;
// 版本检查
isUpdateAvailable.value = _isNewVersionAvailable(resp.systemversion!);
bool isUpdate = _isNewVersionAvailable(resp.systemversion!);
if (isUpdate) {
await _bleManager.setEnterOta(true);
await Future.delayed(const Duration(seconds: 2));
jlota.init();
}
isUpdateAvailable.value = isUpdate;
} catch (e) {
// 统一错误处理
errorMessage.value = e.toString();
Get.snackbar('错误', e.toString(),
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white);
Get.snackbar(
'错误',
e.toString(),
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red[600],
colorText: Colors.white,
icon: const Icon(
Icons.error_outline,
color: Colors.white,
),
);
// 重要:重新抛出异常以便上层处理(可选)
} finally {
isFetchingDeviceInfo.value = false;
@ -235,7 +256,7 @@ class OtaUpgradeController extends GetxController {
}
return false;
} catch (e) {
print('版本比较错误: $e');
Logger().d('版本比较错误: $e');
return false;
}
}
@ -246,15 +267,35 @@ class OtaUpgradeController extends GetxController {
if (latestVersion.value != "") {
_showUpdateDialog(latestVersion.value);
} else {
Get.snackbar('错误', '获取最新版本信息失败');
Get.snackbar(
'错误',
'获取最新版本信息失败',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.orange[800],
colorText: Colors.white,
icon: const Icon(
Icons.error_outline,
color: Colors.white,
),
);
}
} catch (e) {
Get.snackbar('错误', '获取版本信息失败');
Get.snackbar(
'错误',
'获取版本信息失败',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.orange[800],
colorText: Colors.white,
icon: const Icon(
Icons.error_outline,
color: Colors.white,
),
);
}
}
void _showUpdateDialog(String latest) async {
print('显示更新对话框');
Logger().d('显示更新对话框');
Get.dialog(
AlertDialog(
title: Text('发现新版本 $latest'),
@ -262,7 +303,7 @@ class OtaUpgradeController extends GetxController {
actions: [
TextButton(
onPressed: () async {
print('取消按钮被点击');
Logger().d('取消按钮被点击');
Get.back();
Future.delayed(Duration(milliseconds: 300));
@ -271,7 +312,7 @@ class OtaUpgradeController extends GetxController {
),
TextButton(
onPressed: () async {
print('更新按钮被点击');
Logger().d('更新按钮被点击');
Get.back();
Future.delayed(Duration(milliseconds: 300));
@ -339,7 +380,17 @@ class OtaUpgradeController extends GetxController {
}
}
} catch (e) {
Get.snackbar('错误', '检查本地文件失败: $e');
Get.snackbar(
'错误',
'检查本地文件失败: $e',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red[600],
colorText: Colors.white,
icon: Icon(
Icons.error_outline,
color: Colors.white,
),
);
return;
}
@ -361,12 +412,22 @@ class OtaUpgradeController extends GetxController {
}
}
} catch (e) {
Get.snackbar('错误', '删除旧文件失败: $e');
Get.snackbar(
'错误',
'删除旧文件失败: $e',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red[600],
colorText: Colors.white,
icon: Icon(
Icons.error_outline,
color: Colors.white,
),
);
//isUpgrade.value = false;
return;
}
print('文件保存路径: $savePath');
Logger().d('文件保存路径: $savePath');
detailedStatus.value = '开始下载固件包...';
final options = Options(
@ -392,7 +453,17 @@ class OtaUpgradeController extends GetxController {
// Get.snackbar('下载完成', '文件已保存至: $savePath');
detailedStatus.value = '下载完成,准备升级...';
} else {
Get.snackbar('错误', '文件未找到,请检查写入权限');
Get.snackbar(
'错误',
'文件未找到,请检查写入权限',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.orange[600],
colorText: Colors.white,
icon: Icon(
Icons.error_outline,
color: Colors.white,
),
);
detailedStatus.value = '下载失败';
showDetailedStatus.value = false;
return;
@ -401,15 +472,45 @@ class OtaUpgradeController extends GetxController {
startUpgrading(savePath);
return;
} on DioException catch (e) {
Get.snackbar('下载失败', '错误: ${e.message}');
Get.snackbar(
'下载失败',
'错误: ${e.message}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.orange[600],
colorText: Colors.white,
icon: Icon(
Icons.error_outline,
color: Colors.white,
),
);
} on FileSystemException catch (e) {
Get.snackbar('存储失败', '无法写入文件: ${e.message}');
Get.snackbar(
'存储失败',
'无法写入文件: ${e.message}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.orange[600],
colorText: Colors.white,
icon: Icon(
Icons.error_outline,
color: Colors.white,
),
);
} catch (e) {
isDownloading.value = false;
detailedStatus.value = '下载失败,请检查网络连接';
showDetailedStatus.value = false;
Get.snackbar('下载失败', '请检查网络连接: $e');
print('下载失败: $e');
Get.snackbar(
'下载失败',
'请检查网络连接: $e',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.orange[600],
colorText: Colors.white,
icon: Icon(
Icons.error_outline,
color: Colors.white,
),
);
Logger().d('下载失败: $e');
}
}
@ -440,8 +541,12 @@ class OtaUpgradeController extends GetxController {
'电量检查失败',
'无法获取设备信息,请检查设备连接',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.orange,
backgroundColor: Colors.orange[600],
colorText: Colors.white,
icon: Icon(
Icons.warning_amber,
color: Colors.white,
),
);
return false;
}
@ -458,14 +563,36 @@ class OtaUpgradeController extends GetxController {
Get.dialog(
AlertDialog(
title: Text('电量不足'),
content: Text(message),
title: Text(
'电量不足',
style: TextStyle(
color: Colors.orange[700],
fontWeight: FontWeight.w600,
),
),
content: Text(
message,
style: TextStyle(
color: Colors.orange[600],
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text('确定'),
style: TextButton.styleFrom(
foregroundColor: Colors.orange[700],
),
child: const Text('确定'),
),
],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(
color: Colors.orange.withValues(alpha: 0.3),
width: 1,
),
),
backgroundColor: Colors.orange[50],
),
);
return false;
@ -477,8 +604,12 @@ class OtaUpgradeController extends GetxController {
'电量检查失败',
'检查设备电量时发生错误:$e',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
backgroundColor: Colors.orange[600],
colorText: Colors.white,
icon: const Icon(
Icons.warning_amber,
color: Colors.white,
),
);
return false;
}
@ -505,15 +636,83 @@ class OtaUpgradeController extends GetxController {
isUpgrade.value = false;
detailedStatus.value = '升级成功完成';
showDetailedStatus.value = false;
Get.snackbar('升级成功', '请退出重新连接设备');
_getCurrentVersion();
// 升级成功,更新版本信息显示
currentVersion.value = latestVersion.value;
isUpdateAvailable.value = false;
// 显示升级成功对话框,引导用户重新配对
Get.dialog(
AlertDialog(
title: Row(
children: [
Icon(
Icons.check_circle,
color: Colors.green[600],
size: 28,
),
SizedBox(width: 12),
Text(
'升级成功',
style: TextStyle(
color: Colors.green[700],
fontWeight: FontWeight.w600,
),
),
],
),
content: Text(
'固件升级已完成!\n\n设备需要重新配对才能正常使用,现在跳转到配对页面吗?',
style: TextStyle(
fontSize: 16,
height: 1.4,
),
),
actions: [
TextButton(
onPressed: () {
Get.back(); // 关闭对话框
},
child: Text(
'稍后配对',
style: TextStyle(color: Colors.grey[600]),
),
),
ElevatedButton(
onPressed: () {
Get.back(); // 关闭对话框
Get.toNamed(Routes.pairing);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green[600],
foregroundColor: Colors.white,
),
child: Text('立即配对'),
),
],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
barrierDismissible: false, // 防止用户意外关闭
);
}
if (event.event == 'onError') {
type.value = UPGRADE_FAILED;
isUpgrade.value = false;
detailedStatus.value = '升级失败,错误码:${event.code}';
showDetailedStatus.value = false;
Get.snackbar('升级失败', '请检查设备连接或升级包,错误码:${event.code}');
Get.snackbar(
'升级失败',
'请检查设备连接或升级包,错误码:${event.code}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red[600],
colorText: Colors.white,
icon: Icon(
Icons.error_outline,
color: Colors.white,
),
);
}
if (event.event == 'NOCONNECTION') {
type.value = CONNECTION_DISCONNECTION;

96
lib/modules/pairing/controllers/pairing_controller.dart

@ -76,6 +76,9 @@ class PairingController extends GetxController {
bool isShowSkipButton = false; //是否显示跳过按钮
final availableDevices = <BluetoothDevice>[].obs; // 可用设备列表
final selectedDevice = Rx<BluetoothDevice?>(null); // 用户选中的设备
// ========== 监听器管理 ==========
final List<Worker> _workers = [];
Timer? _bleTimeoutTimer;
@ -385,13 +388,22 @@ class PairingController extends GetxController {
void _checkCurrentStatus() async {
try {
// 1. 检查蓝牙状态
if (_bluetoothManager.bluetoothState == 'off') {
final results = await Future.wait([
_checkBluetoothState(), // 检查蓝牙状态
_checkPermissions(), // 检查权限
_bluetoothManager.refreshDeviceConnection(), // 刷新设备连接
]);
final isBluetoothOk = results[0] as bool;
final arePermissionsOk = results[1] as bool;
if (!isBluetoothOk) {
_setError(ErrorType.bluetoothOff, 'pleaseEnableBluetooth'.tr);
return;
}
// 2. 检查权限
if (!await _checkPermissions()) {
if (!arePermissionsOk) {
_setError(ErrorType.noPermission, 'pleaseGrantNecessaryPermissions'.tr);
return;
}
@ -416,6 +428,11 @@ class PairingController extends GetxController {
}
}
/// 新增一个辅助方法来检查蓝牙状态
Future<bool> _checkBluetoothState() async {
return _bluetoothManager.bluetoothState != 'off';
}
/// 检查权限
Future<bool> _checkPermissions() async {
// Android权限检查
@ -553,10 +570,10 @@ class PairingController extends GetxController {
try {
// 设置超时定时器
_bleTimeoutTimer?.cancel();
_bleTimeoutTimer = Timer(const Duration(seconds: 30), () {
_bleTimeoutTimer = Timer(const Duration(seconds: 20), () {
if (_currentState.value == PairingState.waitingBle) {
_setError(ErrorType.bleTimeout,
'bleConnectionTimeoutRetry'.tr); //BLE连接超时,请重试
'bleConnectionTimeoutRetry'.tr); //BLE连接超时,请重启耳机后再试
}
});
@ -587,18 +604,29 @@ class PairingController extends GetxController {
}
}
//自动绑定设备 0 未合作商 1 合作商 2,已绑定
//自动绑定设备 0 未合作商 1 合作商 9经典蓝牙已绑定 10,ble蓝牙已绑定
Future<int> authbindingDevice(BluetoothDevice device) async {
// if (User.instance.earphoneauthcode != "" &&
// User.instance.earphonemac == device.address) {
// // 已经绑定
// return 9;
// }
// 检查设备名称是否包含 DeepSound 或 DeapSound
final deviceName = device.name;
final names = AppConfig.allproductNames();
// 检查是否已有绑定的设备
print("device.address${device.address}");
final existingDevice = User.instance.devices.firstWhere(
(d) => d.devicetype == 1 && d.devicemac == device.address,
orElse: () => UserDevice(
id: -1,
uid: '',
devicetype: -1,
devicename: '',
devicemac: '',
license: ''),
);
if (existingDevice.id != -1) {
// 已经绑定
return 9;
}
print(
'获取到 deviceName: $deviceName address: ${device.address} names:$names');
if (names.contains(deviceName)) {
@ -649,14 +677,26 @@ class PairingController extends GetxController {
_bleManager.stopScan();
deviceCode.value = code;
_verifiedDeviceAddress = device.address;
// 检查是否已经绑定
// if (User.instance.earphonelicense != "" &&
// User.instance.earphonemac == device.address) {
// // 已经绑定
// return 10;
// } else {
return 1;
// }
//检查是否已经绑定
final existingDevice = User.instance.devices.firstWhere(
(d) =>
d.devicetype == 2 &&
d.license != "" &&
d.devicemac == device.address,
orElse: () => UserDevice(
id: -1,
uid: '',
devicetype: -1,
devicename: '',
devicemac: '',
license: ''),
);
if (existingDevice.id != -1) {
// 已经绑定
return 10;
} else {
return 1;
}
}
} else {
break;
@ -667,10 +707,11 @@ class PairingController extends GetxController {
}
await Future.delayed(Duration(seconds: 1));
}
return 0;
} else {
return 0;
return 1;
}
return 0;
//可以跳转1
}
// 新增方法:从hex数据中提取MAC地址并与当前设备MAC对比
@ -794,7 +835,7 @@ class PairingController extends GetxController {
/// 导航到主页
void _navigateToHome() {
Future.delayed(const Duration(seconds: 1), () async {
Future.delayed(const Duration(milliseconds: 500), () async {
// 启动AgentService服务
try {
Logger.i(_tag, '启动AgentService服务');
@ -905,4 +946,13 @@ class PairingController extends GetxController {
Logger.e(_tag, '清除设备关联失败: $e');
}
}
// 用户选择设备后调用
void onDeviceSelected(BluetoothDevice device) {
Logger.i(_tag, '用户选择了设备: ${device.name} (${device.address})');
selectedDevice.value = device;
_currentClassicDevice = device;
_transition(PairingState.verifying);
_verifyDevice(device);
}
}

342
lib/modules/pairing/views/pairing_view.dart

@ -3,9 +3,68 @@ import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../controllers/pairing_controller.dart';
class PairingView extends GetView<PairingController> {
class PairingView extends StatefulWidget {
const PairingView({Key? key}) : super(key: key);
@override
State<PairingView> createState() => _PairingViewState();
}
class _PairingViewState extends State<PairingView>
with TickerProviderStateMixin {
late AnimationController _pulseController;
late AnimationController _rotationController;
late Animation<double> _pulseAnimation;
late Animation<double> _rotationAnimation;
PairingController get controller => Get.find<PairingController>();
@override
void initState() {
super.initState();
// 脉搏动画控制器
_pulseController = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
);
// 旋转动画控制器
_rotationController = AnimationController(
duration: const Duration(milliseconds: 2000),
vsync: this,
);
// 脉搏动画
_pulseAnimation = Tween<double>(
begin: 0.8,
end: 1.2,
).animate(CurvedAnimation(
parent: _pulseController,
curve: Curves.easeInOut,
));
// 旋转动画
_rotationAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _rotationController,
curve: Curves.linear,
));
// 开始动画
_pulseController.repeat(reverse: true);
_rotationController.repeat();
}
@override
void dispose() {
_pulseController.dispose();
_rotationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final bool isDarkMode = Theme.of(context).brightness == Brightness.dark;
@ -30,7 +89,7 @@ class PairingView extends GetView<PairingController> {
padding: EdgeInsets.symmetric(horizontal: 24.w),
// 添加SingleChildScrollView以防止溢出
child: SingleChildScrollView(
physics: BouncingScrollPhysics(),
physics: const BouncingScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight:
@ -98,8 +157,8 @@ class PairingView extends GetView<PairingController> {
final state = controller.currentState;
final isLoading = controller.isLoading.value;
// 根据状态决定动画颜色
Color primaryColor = Color(0xFF0066CC);
Color secondaryColor = Color(0xFF5E5CE6);
Color primaryColor = const Color(0xFF0066CC);
Color secondaryColor = const Color(0xFF5E5CE6);
// 错误状态使用红色
if (state == PairingState.error) {
@ -114,7 +173,7 @@ class PairingView extends GetView<PairingController> {
// 待机状态的特殊处理
if (state == PairingState.idle || state == PairingState.waitingClassic) {
return Container(
return SizedBox(
width: 240.w,
height: 240.w,
child: Stack(
@ -129,8 +188,8 @@ class PairingView extends GetView<PairingController> {
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
primaryColor.withOpacity(0.1),
secondaryColor.withOpacity(0.1),
primaryColor.withValues(alpha: 0.1),
secondaryColor.withValues(alpha: 0.1),
],
),
shape: BoxShape.circle,
@ -159,51 +218,25 @@ class PairingView extends GetView<PairingController> {
);
}
return Container(
return SizedBox(
width: 240.w,
height: 240.w,
child: Stack(
alignment: Alignment.center,
children: [
// 脉搏背景 - 只在加载时显示动画
if (isLoading)
TweenAnimationBuilder<double>(
tween: Tween<double>(begin: 0.8, end: 1.1),
duration: const Duration(seconds: 2),
curve: Curves.easeInOut,
builder: (context, value, child) {
return Container(
width: 200.w * value,
height: 200.w * value,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
primaryColor.withOpacity(0.3),
secondaryColor.withOpacity(0.3),
],
),
shape: BoxShape.circle,
),
);
},
onEnd: () {
// 重新开始动画
},
),
// 连续脉搏动画 - 只在加载时显示
if (isLoading) _buildPulseAnimation(primaryColor, secondaryColor),
// 特殊的安全连接动画 - 只在建立安全连接时显示
if (state == PairingState.waitingBle)
_buildSecureConnectionAnimation(primaryColor, secondaryColor),
// 设备关联动画 - 只在关联设备时显示
if (state == PairingState.associating)
_buildAssociatingAnimation(primaryColor, secondaryColor),
// 旋转边框 - 只在加载时显示
if (isLoading)
SizedBox(
width: 100.w,
height: 100.w,
child: CircularProgressIndicator(
strokeWidth: 4.w,
valueColor: AlwaysStoppedAnimation<Color>(primaryColor),
backgroundColor: Colors.grey.withOpacity(0.2),
),
),
if (isLoading) _buildRotatingBorder(primaryColor),
// 设备图标
Container(
@ -229,6 +262,128 @@ class PairingView extends GetView<PairingController> {
});
}
// 构建连续脉搏动画
Widget _buildPulseAnimation(Color primaryColor, Color secondaryColor) {
return AnimatedBuilder(
animation: _pulseAnimation,
builder: (context, child) {
final value = _pulseAnimation.value;
final opacity = 0.4 - (_pulseController.value * 0.3); // 0.4 到 0.1 的透明度变化
return Container(
width: 200.w * value,
height: 200.w * value,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
primaryColor.withValues(alpha: opacity),
secondaryColor.withValues(alpha: opacity),
],
),
shape: BoxShape.circle,
),
);
},
);
}
// 构建旋转边框动画
Widget _buildRotatingBorder(Color primaryColor) {
return AnimatedBuilder(
animation: _rotationAnimation,
builder: (context, child) {
return Transform.rotate(
angle: _rotationAnimation.value * 2 * 3.14159, // 完整旋转
child: SizedBox(
width: 140.w,
height: 140.w,
child: CircularProgressIndicator(
strokeWidth: 3.w,
valueColor: AlwaysStoppedAnimation<Color>(primaryColor),
backgroundColor: Colors.grey.withValues(alpha: 0.2),
value: 0.7, // 显示70%的圆弧
),
),
);
},
);
}
// 构建安全连接特殊动画 - 多层波纹效果
Widget _buildSecureConnectionAnimation(Color primaryColor, Color secondaryColor) {
return Stack(
alignment: Alignment.center,
children: [
// 第一层波纹
AnimatedBuilder(
animation: _pulseAnimation,
builder: (context, child) {
final value = _pulseAnimation.value;
final opacity = 0.3 - (_pulseController.value * 0.2);
return Container(
width: 180.w * value,
height: 180.w * value,
decoration: BoxDecoration(
border: Border.all(
color: primaryColor.withValues(alpha: opacity),
width: 2.w,
),
shape: BoxShape.circle,
),
);
},
),
// 第二层波纹 - 延迟动画
AnimatedBuilder(
animation: _pulseAnimation,
builder: (context, child) {
final delayedValue = (_pulseController.value + 0.3) % 1.0;
final value = 0.8 + (delayedValue * 0.4);
final opacity = 0.25 - (delayedValue * 0.15);
return Container(
width: 160.w * value,
height: 160.w * value,
decoration: BoxDecoration(
border: Border.all(
color: secondaryColor.withValues(alpha: opacity),
width: 1.5.w,
),
shape: BoxShape.circle,
),
);
},
),
// 第三层波纹 - 更大延迟
AnimatedBuilder(
animation: _pulseAnimation,
builder: (context, child) {
final delayedValue = (_pulseController.value + 0.6) % 1.0;
final value = 0.8 + (delayedValue * 0.4);
final opacity = 0.2 - (delayedValue * 0.1);
return Container(
width: 140.w * value,
height: 140.w * value,
decoration: BoxDecoration(
border: Border.all(
color: primaryColor.withValues(alpha: opacity),
width: 1.w,
),
shape: BoxShape.circle,
),
);
},
),
],
);
}
// 根据状态获取图标
IconData _getIconForState(PairingState state) {
switch (state) {
@ -352,7 +507,7 @@ class PairingView extends GetView<PairingController> {
// 已连接设备信息
Widget _buildConnectedDevice(device, bool isDarkMode) {
return Container(
return SizedBox(
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -443,7 +598,7 @@ class PairingView extends GetView<PairingController> {
final showSkipButton = controller.showSkipButton.value;
final showAutoCodeInput = controller.showAutoCodeInput.value;
final showSkipBindingButton = controller.showSkipBindingButton.value;
return Container(
return SizedBox(
width: double.infinity,
child: Column(
children: [
@ -452,7 +607,7 @@ class PairingView extends GetView<PairingController> {
ElevatedButton(
onPressed: () => controller.onConnectTap(),
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF0066CC),
backgroundColor: const Color(0xFF0066CC),
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
@ -572,7 +727,7 @@ class PairingView extends GetView<PairingController> {
TextButton(
onPressed: () => controller.onSkipTap(),
style: TextButton.styleFrom(
foregroundColor: Color(0xFF0066CC),
foregroundColor: const Color(0xFF0066CC),
padding: EdgeInsets.symmetric(vertical: 12.h),
minimumSize: Size(double.infinity, 48.h),
),
@ -592,7 +747,7 @@ class PairingView extends GetView<PairingController> {
TextButton(
onPressed: () => controller.onSkipBindingTap(),
style: TextButton.styleFrom(
foregroundColor: Color(0xFF0066CC),
foregroundColor: const Color(0xFF0066CC),
padding: EdgeInsets.symmetric(vertical: 12.h),
minimumSize: Size(double.infinity, 48.h),
),
@ -611,7 +766,7 @@ class PairingView extends GetView<PairingController> {
TextButton(
onPressed: () => controller.onRetryTap(),
style: TextButton.styleFrom(
foregroundColor: Color(0xFF0066CC),
foregroundColor: const Color(0xFF0066CC),
padding: EdgeInsets.symmetric(vertical: 12.h),
minimumSize: Size(double.infinity, 48.h),
),
@ -629,4 +784,89 @@ class PairingView extends GetView<PairingController> {
);
});
}
// 构建设备关联动画 - 连接线条效果
Widget _buildAssociatingAnimation(Color primaryColor, Color secondaryColor) {
return AnimatedBuilder(
animation: _rotationAnimation,
builder: (context, child) {
return Stack(
alignment: Alignment.center,
children: [
// 旋转的连接线条
Transform.rotate(
angle: _rotationAnimation.value * 2 * 3.14159,
child: Container(
width: 160.w,
height: 160.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: primaryColor.withValues(alpha: 0.6),
width: 2.w,
),
),
child: Stack(
children: [
// 连接点1
Positioned(
top: 10.w,
left: 80.w - 4.w,
child: Container(
width: 8.w,
height: 8.w,
decoration: BoxDecoration(
color: primaryColor,
shape: BoxShape.circle,
),
),
),
// 连接点2
Positioned(
bottom: 10.w,
left: 80.w - 4.w,
child: Container(
width: 8.w,
height: 8.w,
decoration: BoxDecoration(
color: secondaryColor,
shape: BoxShape.circle,
),
),
),
// 连接点3
Positioned(
top: 80.w - 4.w,
left: 10.w,
child: Container(
width: 8.w,
height: 8.w,
decoration: BoxDecoration(
color: primaryColor,
shape: BoxShape.circle,
),
),
),
// 连接点4
Positioned(
top: 80.w - 4.w,
right: 10.w,
child: Container(
width: 8.w,
height: 8.w,
decoration: BoxDecoration(
color: secondaryColor,
shape: BoxShape.circle,
),
),
),
],
),
),
),
],
);
},
);
}
}

7
lib/modules/pairing/views/pairing_view_new.dart

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../controllers/pairing_controller.dart';
import '../../../core/themes/app_theme.dart';
/// 配对视图 - 简化版本
class PairingView extends GetView<PairingController> {
@ -94,9 +93,9 @@ class PairingView extends GetView<PairingController> {
Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
color: Colors.red.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12.r),
border: Border.all(color: Colors.red.withOpacity(0.3)),
border: Border.all(color: Colors.red.withValues(alpha: 0.3)),
),
child: Row(
children: [
@ -184,7 +183,7 @@ class PairingView extends GetView<PairingController> {
width: 100.w,
height: 100.w,
decoration: BoxDecoration(
color: iconColor.withOpacity(0.1),
color: iconColor.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: Icon(

64
lib/modules/profile/controllers/profile_controller.dart

@ -1,4 +1,4 @@
import 'package:deep_voice/data/services/user_portrait.dart';
import '/data/services/user_portrait.dart';
import 'package:dio/dio.dart' as dio;
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
@ -31,6 +31,7 @@ class ProfileController extends GetxController {
final RxBool hasEdits = false.obs; // 是否有编辑
final RxBool isUpdating = false.obs; // 是否正在更新用户信息
final RxBool isEditingNickname = false.obs; // 是否正在编辑昵称(内联编辑状态)
final RxBool isAvatarReady = false.obs; // 头像数据是否已准备好,用于防止闪烁
//音乐播放管理
final _userportraitManager = Get.find<UserportraitManager>();
// 用于乐观更新的原始状态备份
@ -91,7 +92,7 @@ class ProfileController extends GetxController {
currentLanguage.value = languageCode;
}
} catch (e) {
print('Failed to load language preference: $e');
Logger().e('Failed to load language preference: $e');
}
}
@ -101,7 +102,7 @@ class ProfileController extends GetxController {
await _storage.write('languageCode', locale.languageCode);
await _storage.write('countryCode', locale.countryCode);
} catch (e) {
print('Failed to save language preference: $e');
Logger().e('Failed to save language preference: $e');
}
}
@ -191,6 +192,15 @@ class ProfileController extends GetxController {
// }
// }
// 强制回滚未保存的更改(在页面退出时调用)
void forceRollbackIfUnsaved() {
if (hasEdits.value) {
Logger().d('检测到未保存的编辑,执行强制回滚');
_rollbackToOriginalState(); // 复用现有的回滚方法
}
isEditingNickname.value = false;
}
// 更新用户信息到后端
Future<void> _syncWithBackend() async {
try {
@ -289,19 +299,23 @@ class ProfileController extends GetxController {
}
}
void loadUserInfo() {
void loadUserInfo() async {
final userJson = _storage.read(userKey);
if (userJson != null) {
userInfo.value = User.instance;
Logger().d('从本地加载用户信息: $userJson');
// 加载本地头像路径并验证文件有效性
// 重置头像准备状态
isAvatarReady.value = false;
// 同步加载本地头像路径并验证文件有效性 - 修复闪烁问题
final savedLocalAvatar = _storage.read('local_avatar');
if (savedLocalAvatar != null && savedLocalAvatar.toString().isNotEmpty) {
final avatarPath = savedLocalAvatar.toString();
// 验证文件是否存在
// 同步验证文件是否存在,避免异步导致的闪烁
final file = File(avatarPath);
file.exists().then((exists) {
try {
final exists = await file.exists();
if (exists) {
localAvatar.value = avatarPath;
Logger().d('加载本地头像路径: ${localAvatar.value}');
@ -311,18 +325,26 @@ class ProfileController extends GetxController {
_storage.remove('local_avatar');
Logger().w('本地头像文件不存在,已清除路径: $avatarPath');
}
});
} catch (e) {
// 文件访问异常,清除无效路径
localAvatar.value = '';
_storage.remove('local_avatar');
Logger().e('验证本地头像文件时发生异常: $e');
}
} else {
// 确保本地头像路径为空
localAvatar.value = '';
Logger().d('没有本地头像路径');
}
// 头像数据准备完成
isAvatarReady.value = true;
// 保存原始状态用于回滚
_saveOriginalState();
// 如果有远程头像地址,下载并保存到本地
if (userInfo.value.avatar.isNotEmpty) {
// 如果有远程头像地址且本地头像不存在,才下载并保存到本地
if (userInfo.value.avatar.isNotEmpty && localAvatar.value.isEmpty) {
Logger().d('远程头像地址: ${userInfo.value.avatar}');
_downloadAndSaveAvatar(userInfo.value.avatar);
}
@ -333,11 +355,12 @@ class ProfileController extends GetxController {
}
// 确保本地状态与服务器状态的一致性
void _ensureStateConsistency() {
// 检查本地头像文件是否存在
void _ensureStateConsistency() async {
// 同步检查本地头像文件是否存在 - 修复闪烁问题
if (localAvatar.value.isNotEmpty) {
final file = File(localAvatar.value);
file.exists().then((exists) {
try {
final exists = await file.exists();
if (!exists) {
Logger().w('本地头像文件不存在,清除本地头像路径: ${localAvatar.value}');
localAvatar.value = '';
@ -348,7 +371,11 @@ class ProfileController extends GetxController {
_downloadAndSaveAvatar(userInfo.value.avatar);
}
}
});
} catch (e) {
Logger().e('检查本地头像文件一致性时发生异常: $e');
localAvatar.value = '';
_storage.remove('local_avatar');
}
}
}
@ -577,15 +604,6 @@ class ProfileController extends GetxController {
_saveOriginalState();
await _syncLocalStorage();
// 同步更新 SettingsController
if (Get.isRegistered<SettingsController>()) {
final settingsController = Get.find<SettingsController>();
settingsController.userInfo.value.name = userInfo.value.name;
settingsController.userInfo.value.avatar =
'${userInfo.value.avatar}?time=${DateTime.now().millisecondsSinceEpoch}';
settingsController.userInfo.refresh();
}
// 重置编辑状态
hasEdits.value = false;

488
lib/modules/profile/views/profile_meeting_view.dart

@ -0,0 +1,488 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../controllers/profile_controller.dart';
import 'unified_avatar.dart';
class ProfileMeetingView extends GetView<ProfileController> {
ProfileMeetingView({super.key});
// 昵称编辑控制器 - 与 profile_view.dart 保持一致
final TextEditingController nameController = TextEditingController();
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.white,
appBar: _buildAppBar(isDarkMode),
body: _buildBody(isDarkMode),
);
}
PreferredSizeWidget _buildAppBar(bool isDarkMode) {
// 初始化 nameController 的文本内容
nameController.text = controller.userInfo.value.name;
return AppBar(
backgroundColor: isDarkMode ? Colors.black : Colors.white,
elevation: 0,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: isDarkMode ? Colors.white : Colors.black87,
size: 20.w,
),
onPressed: () {
controller.forceRollbackIfUnsaved();
Get.back();
},
),
title: Text(
'账号和资料',
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
fontSize: 18.sp,
fontWeight: FontWeight.w600,
),
),
centerTitle: true,
actions: [
Obx(() => Visibility(
visible: controller.hasEdits.value,
child: controller.isUpdating.value
? Padding(
padding: EdgeInsets.all(16.w),
child: SizedBox(
width: 20.w,
height: 20.w,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
isDarkMode ? Colors.blue[300]! : Colors.blue,
),
),
),
)
: TextButton(
onPressed: () async {
// 保存用户名和头像
await controller
.updateUserNameAndAvatar(nameController.text);
// 如果保存成功(没有编辑状态),延迟返回上一级页面让用户看到成功提示
if (!controller.hasEdits.value) {
// 延迟跳转,让用户看到成功提示
Future.delayed(const Duration(milliseconds: 500), () {
if (Get.isRegistered<ProfileController>()) {
Get.back();
}
});
}
},
child: Text(
'save'.tr,
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.blue[300] : Colors.blue,
fontWeight: FontWeight.w500,
),
),
),
))
],
);
}
Widget _buildBody(bool isDarkMode) {
return WillPopScope(
onWillPop: () async {
controller.forceRollbackIfUnsaved();
return true;
},
child: SafeArea(
child: ListView(
padding: EdgeInsets.symmetric(horizontal: 16.w),
children: [
SizedBox(height: 4.h),
_buildAvatarSection(isDarkMode),
SizedBox(height: 16.h),
_buildNicknameItem(isDarkMode),
Divider(
height: 0.5.h,
color: isDarkMode ? Colors.grey[800] : Colors.grey[200]),
_buildLoginMethodItem(isDarkMode),
Divider(
height: 0.5.h,
color: isDarkMode ? Colors.grey[800] : Colors.grey[200]),
_buildManageLoginItem(isDarkMode),
Divider(
height: 0.5.h,
color: isDarkMode ? Colors.grey[800] : Colors.grey[200]),
SizedBox(height: 32.h),
_buildLogoutButton(isDarkMode),
SizedBox(height: 16.h),
_buildDeleteAccountButton(isDarkMode),
SizedBox(height: 8.h),
_buildWarningText(),
SizedBox(height: 32.h),
],
),
));
}
Widget _buildAvatarSection(bool isDarkMode) {
return Center(
child: Column(
children: [
// 使用统一头像组件,避免闪烁问题
GestureDetector(
onTap: () => controller.updateUserAvatar(),
child: Stack(
children: [
UnifiedAvatar(
radius: 40.r,
backgroundColor:
isDarkMode ? Colors.grey[800] : Colors.grey[200],
iconColor: isDarkMode ? Colors.grey[400] : Colors.grey[600],
iconSize: 30.r,
isDarkMode: isDarkMode,
),
Positioned(
bottom: 0,
right: 0,
child: Container(
width: 22.w,
height: 22.w,
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[600] : Colors.grey[350],
shape: BoxShape.circle,
border: Border.all(
color: isDarkMode ? Colors.black : Colors.white,
width: 0.w,
),
),
child: Icon(
Icons.camera_alt,
size: 14.w,
color: Colors.black,
),
),
),
],
),
),
],
),
);
}
// 昵称设置项(双模式内联编辑)- 与 profile_view.dart 完全相同的逻辑
Widget _buildNicknameItem(bool isDarkMode) {
return Obx(() {
// 根据编辑状态显示不同的UI
if (controller.isEditingNickname.value) {
return _buildNicknameEditingMode(isDarkMode, nameController);
} else {
return _buildNicknameDisplayMode(isDarkMode, nameController);
}
});
}
// 构建昵称显示模式 - 适配 profile_meeting_view 的 ListTile 样式
Widget _buildNicknameDisplayMode(
bool isDarkMode, TextEditingController nameController) {
return ListTile(
onTap: () {
// 开始内联编辑
nameController.text = controller.userInfo.value.name;
controller.startEditingNickname();
},
// 禁用水波纹效果
splashColor: Colors.transparent,
title: Text(
'nickname'.tr,
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Obx(() => Text(
controller.userInfo.value.name.isNotEmpty
? controller.userInfo.value.name
: '未设置昵称',
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.white : Colors.grey[600],
),
)),
SizedBox(width: 8.w),
Icon(
Icons.arrow_forward_ios,
size: 16.w,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
],
),
);
}
// 构建昵称编辑模式 - 适配 profile_meeting_view 的 ListTile 样式
Widget _buildNicknameEditingMode(
bool isDarkMode, TextEditingController nameController) {
return ListTile(
title: Text(
'nickname'.tr,
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
// 编辑输入框 - 替换原来的昵称文本显示区域
Container(
constraints: BoxConstraints(
minWidth: 80.w, // 确保有足够的编辑空间
maxWidth: 120.w, // 限制最大宽度保持布局稳定
),
child: TextField(
controller: nameController,
autofocus: true,
textAlign: TextAlign.right, // 右对齐,与原昵称显示保持一致
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.white : Colors.grey[600],
),
decoration: InputDecoration(
hintText: '输入昵称',
hintStyle: TextStyle(
color: isDarkMode ? Colors.grey[500] : Colors.grey[400],
fontSize: 16.sp,
),
border: InputBorder.none, // 移除边框,保持简洁
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
contentPadding: EdgeInsets.zero, // 移除内边距
isDense: true,
),
onSubmitted: (value) {
// 按回车确认编辑
controller.finishEditingNickname(value);
},
onTapOutside: (event) {
// 点击外部区域确认编辑
controller.finishEditingNickname(nameController.text);
},
),
),
SizedBox(width: 8.w),
// 保持原有的箭头图标位置,但在编辑模式下可以考虑隐藏或改变
Icon(
Icons.arrow_forward_ios,
size: 16.w,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
],
),
);
}
Widget _buildLoginMethodItem(bool isDarkMode) {
return ListTile(
title: Text(
'登录方式',
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
),
trailing: Icon(
Icons.arrow_forward_ios,
size: 16.w,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
);
}
Widget _buildManageLoginItem(bool isDarkMode) {
return ListTile(
title: Text(
'管理我的登录',
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
),
trailing: Icon(
Icons.arrow_forward_ios,
size: 16.w,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
);
}
Widget _buildLogoutButton(bool isDarkMode) {
return SizedBox(
width: double.infinity,
height: 48.h,
child: OutlinedButton(
onPressed: () => _showLogoutDialog(isDarkMode),
style: OutlinedButton.styleFrom(
backgroundColor: isDarkMode ? Colors.grey[700] : Colors.grey[200],
side: BorderSide.none,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
child: Text(
'退出登录',
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.white : Colors.black87,
),
),
),
);
}
Widget _buildDeleteAccountButton(bool isDarkMode) {
return SizedBox(
width: double.infinity,
height: 48.h,
child: OutlinedButton(
onPressed: () => _showDeleteAccountDialog(isDarkMode),
style: OutlinedButton.styleFrom(
side: BorderSide.none,
backgroundColor: isDarkMode ? Colors.grey[700] : Colors.grey[200],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
child: Text(
'删除账号',
style: TextStyle(
fontSize: 16.sp,
color: Colors.red[600],
),
),
),
);
}
Widget _buildWarningText() {
return Text.rich(
TextSpan(
children: [
TextSpan(
text: '警告:',
style: TextStyle(
fontSize: 12.sp, // 稍微大一点的字号
color: Colors.red[800], // 更深的红色
//fontStyle: FontStyle.italic, // 斜体
),
),
TextSpan(
text: '您的账号信息和所有文件将被永久删除。',
style: TextStyle(
fontSize: 11.sp,
color: Colors.grey[600],
),
),
],
),
textAlign: TextAlign.center,
);
}
//退出登录提示窗
void _showLogoutDialog(bool isDarkMode) {
Get.dialog(
AlertDialog(
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white,
title: Text(
'退出登录',
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
),
),
content: Text(
'确定要退出当前账号吗?\n请确认所有转写任务已完成。',
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'cancel'.tr,
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
),
),
TextButton(
onPressed: () {
Get.back();
controller.signOut();
},
child: Text(
'confirm'.tr,
style: const TextStyle(
color: Colors.red,
),
),
),
],
),
);
}
void _showDeleteAccountDialog(bool isDarkMode) {
Get.dialog(
AlertDialog(
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white,
title: Text(
'删除账号',
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
),
),
content: Text(
'警告:您的账号信息和所有文件将被永久删除。',
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'取消',
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
),
),
TextButton(
onPressed: () {
Get.back();
controller.deleteAccount();
},
child: Text(
'confirm'.tr,
style: const TextStyle(
color: Colors.red,
),
),
),
],
),
);
}
}

54
lib/modules/profile/views/profile_view.dart

@ -7,6 +7,7 @@ import '../../../routes/app_routes.dart';
import 'dart:io';
import 'dart:ui';
import '../../common/webview_page.dart';
import 'unified_avatar.dart';
// ProfileView: 用户设置页面
// class ProfileView extends GetView<ProfileController> {
@ -157,7 +158,10 @@ class EditProfileView extends GetView<ProfileController> {
size: 20.sp,
color: isDarkMode ? Colors.white : Colors.black,
),
onPressed: () => Get.back(),
onPressed: () {
controller.forceRollbackIfUnsaved();
Get.back();
},
),
// title: Text(
// 'editProfile'.tr,
@ -213,15 +217,23 @@ class EditProfileView extends GetView<ProfileController> {
))
],
),
body: Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
child:
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
_buildEditProfileSection(context, isDarkMode, nameController),
SizedBox(height: 16.h),
_buildAccountSettings(isDarkMode),
SizedBox(height: 16.h),
])));
// ignore: deprecated_member_use
body: WillPopScope(
onWillPop: () async {
ProfileController.to.forceRollbackIfUnsaved();
return true;
},
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildEditProfileSection(
context, isDarkMode, nameController),
SizedBox(height: 16.h),
_buildAccountSettings(isDarkMode),
SizedBox(height: 16.h),
]))));
}
// 新增:构建编辑资料区域的方法
@ -326,20 +338,14 @@ class EditProfileView extends GetView<ProfileController> {
// --- 右侧内容保持不变 ---
Row(
children: [
Obx(() => CircleAvatar(
radius: 26.r, // 调整头像大小以适应行高
backgroundImage: controller.localAvatar.value.isNotEmpty
? FileImage(File(controller.localAvatar.value))
: (controller.userInfo.value.avatar.isNotEmpty
? NetworkImage(controller.userInfo.value.avatar)
: null) as ImageProvider<Object>?,
child: (controller.localAvatar.value.isEmpty &&
controller.userInfo.value.avatar.isEmpty)
? Icon(Icons.person,
size: 20.r, color: Colors.grey) // 默认图标
: null,
backgroundColor: Colors.grey[200], // 添加背景色防止透明图片问题
)),
// 使用统一头像组件,避免闪烁问题
UnifiedAvatar(
radius: 26.r,
backgroundColor: Colors.grey[200],
iconColor: Colors.grey,
iconSize: 20.r,
isDarkMode: isDarkMode,
),
SizedBox(width: 8.w),
Container(
width: 32.w,

137
lib/modules/profile/views/unified_avatar.dart

@ -0,0 +1,137 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../controllers/profile_controller.dart';
/// 统一头像显示组件
/// 确保所有页面的头像显示逻辑一致,避免闪烁问题
class UnifiedAvatar extends StatelessWidget {
final double radius;
final Color? backgroundColor;
final Color? iconColor;
final double? iconSize;
final bool isDarkMode;
final VoidCallback? onTap;
const UnifiedAvatar({
super.key,
required this.radius,
this.backgroundColor,
this.iconColor,
this.iconSize,
this.isDarkMode = false,
this.onTap,
});
@override
Widget build(BuildContext context) {
final ProfileController profileCtrl = Get.find<ProfileController>();
return GestureDetector(
onTap: onTap,
child: Obx(() {
// 等待头像数据准备完成,避免闪烁
if (!profileCtrl.isAvatarReady.value) {
return _buildLoadingAvatar();
}
return _buildAvatar(profileCtrl);
}),
);
}
/// 构建加载中的头像占位符
Widget _buildLoadingAvatar() {
return CircleAvatar(
radius: radius,
backgroundColor:
backgroundColor ?? (isDarkMode ? Colors.grey[800] : Colors.grey[200]),
child: Icon(
Icons.person,
size: iconSize ?? (radius * 0.6),
color: iconColor ?? (isDarkMode ? Colors.grey[400] : Colors.grey[600]),
),
);
}
/// 构建实际头像
Widget _buildAvatar(ProfileController profileCtrl) {
ImageProvider<Object>? imageProvider;
// 统一头像显示优先级:本地头像 > 网络头像 > 默认图标
if (profileCtrl.localAvatar.value.isNotEmpty) {
imageProvider = FileImage(File(profileCtrl.localAvatar.value));
} else if (profileCtrl.userInfo.value.avatar.isNotEmpty) {
imageProvider = NetworkImage(profileCtrl.userInfo.value.avatar);
}
return CircleAvatar(
radius: radius,
backgroundImage: imageProvider,
backgroundColor:
backgroundColor ?? (isDarkMode ? Colors.grey[800] : Colors.grey[200]),
child: (profileCtrl.localAvatar.value.isEmpty &&
profileCtrl.userInfo.value.avatar.isEmpty)
? Icon(
Icons.person,
size: iconSize ?? (radius * 0.6),
color: iconColor ??
(isDarkMode ? Colors.grey[400] : Colors.grey[600]),
)
: null,
);
}
}
/// 带容器装饰的统一头像组件
class UnifiedAvatarWithContainer extends StatelessWidget {
final double width;
final double height;
final double radius;
final Color? backgroundColor;
final Color? iconColor;
final double? iconSize;
final bool isDarkMode;
final VoidCallback? onTap;
final EdgeInsetsGeometry? margin;
final List<BoxShadow>? boxShadow;
const UnifiedAvatarWithContainer({
super.key,
required this.width,
required this.height,
required this.radius,
this.backgroundColor,
this.iconColor,
this.iconSize,
this.isDarkMode = false,
this.onTap,
this.margin,
this.boxShadow,
});
@override
Widget build(BuildContext context) {
return Container(
width: width,
height: height,
margin: margin,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(radius),
boxShadow: boxShadow,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(radius),
child: UnifiedAvatar(
radius: radius / 2,
backgroundColor: backgroundColor,
iconColor: iconColor,
iconSize: iconSize,
isDarkMode: isDarkMode,
onTap: onTap,
),
),
);
}
}

8
lib/modules/settings/bindings/settings_binding.dart

@ -1,11 +1,11 @@
import 'package:get/get.dart';
import '../../profile/controllers/profile_controller.dart';
import '../controllers/settings_controller.dart';
class SettingsBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<SettingsController>(
() => SettingsController(),
);
Get.lazyPut<SettingsController>(() => SettingsController());
Get.lazyPut<ProfileController>(() => ProfileController());
}
}
}

165
lib/modules/settings/controllers/settings_controller.dart

@ -121,10 +121,8 @@ class SettingsController extends GetxController {
RxString timbreName = 'defaultTimbre'.obs;
//用户信息
static const String userKey = 'user_info';
//final _storage=GetStorage();
//final ImagePicker _picker = ImagePicker();
final Rx<User> userInfo = User.instance.obs; //使用单例实例
// static const String userKey = 'user_info';
// final Rx<User> userInfo = User.instance.obs; //使用单例实例
// 添加版本信息变量
final appVersion = ''.obs;
@ -197,14 +195,6 @@ class SettingsController extends GetxController {
];
// 检查单个权限状态
Future<PermissionStatus> checkPermission(Permission permission) async {
return await permission.status;
}
// 请求单个权限
Future<PermissionStatus> requestPermission(Permission permission) async {
return await permission.request();
}
// 打开应用设置
void openSettings() {
@ -244,7 +234,7 @@ class SettingsController extends GetxController {
_loadCurrentSpeechServiceType();
//获取用户信息
loadUserInfo();
// loadUserInfo();
// 获取应用版本信息
getAppVersion();
@ -264,71 +254,71 @@ class SettingsController extends GetxController {
}
}
void loadUserInfo() {
final userJson = _storage.read(userKey);
Logger().d('加载用户信息: $userJson');
if (userJson != null) {
userInfo.value = User.instance;
Logger().d('用户信息已加载: ${userInfo.value.toJson()}');
if (userInfo.value.avatar.isNotEmpty) {
Logger().d('开始下载头像');
_downloadAndSaveAvatar(userInfo.value.avatar);
} else {
Logger().d('用户没有头像');
}
} else {
Logger().d('没有找到用户信息');
}
}
// void loadUserInfo() {
// final userJson = _storage.read(userKey);
// Logger().d('加载用户信息: $userJson');
// if (userJson != null) {
// userInfo.value = User.instance;
// Logger().d('用户信息已加载: ${userInfo.value.toJson()}');
// if (userInfo.value.avatar.isNotEmpty) {
// Logger().d('开始下载头像');
// _downloadAndSaveAvatar(userInfo.value.avatar);
// } else {
// Logger().d('用户没有头像');
// }
// } else {
// Logger().d('没有找到用户信息');
// }
// }
Future<void> saveUserInfo() async {
await _storage.write(userKey, userInfo.value.toJson());
}
// Future<void> saveUserInfo() async {
// await _storage.write(userKey, userInfo.value.toJson());
// }
// 下载并保存远程头像
Future<void> _downloadAndSaveAvatar(String url) async {
try {
Logger().d('开始下载头像: $url');
final dio = Dio(BaseOptions(
headers: {
'accept': '*/*',
'Authorization': _storage.read<String>("logintoken"),
},
));
final response = await dio.get(
url,
options: Options(responseType: ResponseType.bytes),
);
Logger().d('下载状态: ${response.statusCode}');
if (response.statusCode == 200) {
// 保存到应用文档目录
final appDir = await getApplicationSupportDirectory();
final fileName =
'/UserAvatar/avatar_${DateTime.now().millisecondsSinceEpoch}.jpg';
final localPath = '${appDir.path}/$fileName';
// 确保目录存在
final file = File(localPath);
await file.parent.create(recursive: true);
await file.writeAsBytes(response.data);
// 更新本地头像路径
//localAvatar.value = localPath;
await _storage.write('local_avatar', localPath);
Logger().d('头像已下载到本地: $localPath');
//Get.snackbar('头像', '头像已下载到本地');
}
} catch (e) {
Logger().e('下载头像失败: $e');
}
}
// Future<void> _downloadAndSaveAvatar(String url) async {
// try {
// Logger().d('开始下载头像: $url');
// final dio = Dio(BaseOptions(
// headers: {
// 'accept': '*/*',
// 'Authorization': _storage.read<String>("logintoken"),
// },
// ));
// final response = await dio.get(
// url,
// options: Options(responseType: ResponseType.bytes),
// );
// Logger().d('下载状态: ${response.statusCode}');
// if (response.statusCode == 200) {
// // 保存到应用文档目录
// final appDir = await getApplicationSupportDirectory();
// final fileName =
// '/UserAvatar/avatar_${DateTime.now().millisecondsSinceEpoch}.jpg';
// final localPath = '${appDir.path}/$fileName';
// // 确保目录存在
// final file = File(localPath);
// await file.parent.create(recursive: true);
// await file.writeAsBytes(response.data);
// // 更新本地头像路径
// //localAvatar.value = localPath;
// await _storage.write('local_avatar', localPath);
// Logger().d('头像已下载到本地: $localPath');
// //Get.snackbar('头像', '头像已下载到本地');
// }
// } catch (e) {
// Logger().e('下载头像失败: $e');
// }
// }
// Future<void> updateUserAvatar() async {
// final XFile? image = await _picker.pickImage(
@ -348,16 +338,16 @@ class SettingsController extends GetxController {
// }
// }
Future<void> updateUserName(String name) async {
if (name.trim().isNotEmpty) {
userInfo.update((user) {
if (user != null) {
user.name = name.trim();
}
});
await saveUserInfo();
}
}
// Future<void> updateUserName(String name) async {
// if (name.trim().isNotEmpty) {
// userInfo.update((user) {
// if (user != null) {
// user.name = name.trim();
// }
// });
// await saveUserInfo();
// }
// }
// Future<void> updateDeviceName(String deviceName) async {
// userInfo.update((user) {
@ -514,8 +504,9 @@ class SettingsController extends GetxController {
// 显示已是最新版本的提示
Get.snackbar(
'checkUpdate'.tr,
'currentLatestVersion' //当前已是最新版本
.trParams({'version': updateInfo.currentVersion}),
'currentLatestVersion'
.tr //当前已是最新版本
.replaceAll('{version}', updateInfo.currentVersion),
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green.withOpacity(0.8),
colorText: Colors.white,

201
lib/modules/settings/views/settings_view.dart

@ -3,13 +3,15 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'dart:ui';
import 'dart:io';
import '../../../core/theme/app_colors.dart';
import '../controllers/settings_controller.dart';
import '../../../core/widgets/common_bottom_nav.dart';
import '../../../routes/app_routes.dart';
//import '../../../data/models/user_Info.dart';
import 'dart:io';
import '../../common/webview_page.dart';
import '../../../modules/profile/controllers/profile_controller.dart';
import '../../profile/views/unified_avatar.dart';
class SettingsView extends GetView<SettingsController> {
const SettingsView({Key? key}) : super(key: key);
@ -60,6 +62,8 @@ class SettingsView extends GetView<SettingsController> {
// 用户信息卡片
Widget _buildUserInfoCard(bool isDarkMode) {
final ProfileController profileCtrl =
Get.find<ProfileController>(); // 从GetX中获取ProfileController实例
return GestureDetector(
onTap: () => Get.toNamed(Routes.editProfile),
child: Container(
@ -95,77 +99,24 @@ class SettingsView extends GetView<SettingsController> {
: ImageFilter.blur(sigmaX: 0, sigmaY: 0),
child: Row(
children: [
Obx(() => GestureDetector(
//onTap: ()=>controller.updateUserAvatar(),
child: Container(
//用户头像部分
width: 60.w,
height: 60.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: ClipRRect(
// 裁剪圆形头像
borderRadius: BorderRadius.circular(30.r),
child: controller.userInfo.value.avatar.isEmpty
? Container(
decoration: BoxDecoration(
color: Colors.blue[600],
borderRadius: BorderRadius.circular(30.r),
),
child: Center(
child: Text(
controller
.userInfo.value.name.isNotEmpty
? controller.userInfo.value.name[0]
.toUpperCase()
: '?',
style: TextStyle(
color: Colors.white,
fontSize: 24.sp,
fontWeight: FontWeight.bold,
),
),
),
)
: Image.network(
controller.userInfo.value.avatar,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
// 当图片加载失败时显示用户名首字母
return Container(
decoration: BoxDecoration(
color: Colors.blue[600],
borderRadius:
BorderRadius.circular(30.r),
),
child: Center(
child: Text(
controller.userInfo.value.name
.isNotEmpty
? controller
.userInfo.value.name[0]
.toUpperCase()
: '?',
style: TextStyle(
color: Colors.white,
fontSize: 24.sp,
fontWeight: FontWeight.bold,
),
),
),
);
},
)),
),
)),
// 使用统一头像组件,避免闪烁问题
UnifiedAvatarWithContainer(
width: 60.w,
height: 60.w,
radius: 30.r,
backgroundColor:
isDarkMode ? Colors.grey[800] : Colors.grey[200],
iconColor: isDarkMode ? Colors.grey[400] : Colors.grey[600],
iconSize: 30.r,
isDarkMode: isDarkMode,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
SizedBox(width: 16.w),
Expanded(
// 占据剩余空间
@ -175,7 +126,7 @@ class SettingsView extends GetView<SettingsController> {
children: [
Text(
//用户名
controller.userInfo.value.name,
profileCtrl.userInfo.value.name,
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
@ -200,7 +151,7 @@ class SettingsView extends GetView<SettingsController> {
2: 'proUser', //'专业用户'
};
final membershipString = membershipMap[
controller.userInfo.value.viplv] ??
profileCtrl.userInfo.value.viplv] ??
'Unknown';
return Text(
membershipString.tr, // 会员等级
@ -228,7 +179,7 @@ class SettingsView extends GetView<SettingsController> {
),
SizedBox(height: 4.h),
Text(
'ID:${controller.userInfo.value.uid}',
'ID:${profileCtrl.userInfo.value.uid}',
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
@ -456,6 +407,8 @@ class SettingsView extends GetView<SettingsController> {
? Colors.white.withOpacity(0.1)
: Colors.grey[200]),
// 用户反馈
_buildSimpleNavigationSetting(
title: 'feedback'.tr, // 用户反馈
@ -715,8 +668,8 @@ class SettingsView extends GetView<SettingsController> {
? ImageFilter.blur(sigmaX: 5, sigmaY: 5)
: ImageFilter.blur(sigmaX: 0, sigmaY: 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// 左侧应用信息区域 - 使用固定宽度
Row(
children: [
Container(
@ -770,36 +723,48 @@ class SettingsView extends GetView<SettingsController> {
),
],
),
InkWell(
onTap: () {
// 调用检查更新方法
controller.checkForUpdate();
},
child: Container(
constraints: BoxConstraints(
minWidth: 100.w,
maxWidth: 180.w,
),
padding: EdgeInsets.symmetric(
horizontal: 16.w,
vertical: 8.h,
),
decoration: BoxDecoration(
color: isDarkMode
? Colors.blue[900]!.withOpacity(0.3)
: Colors.blue[100],
borderRadius: BorderRadius.circular(16.r),
),
child: Text(
'checkUpdates'.tr, // 检查更新
style: TextStyle(
fontSize: 12.sp,
color:
isDarkMode ? Colors.blue[300] : Colors.blue[600],
// 间距
SizedBox(width: 16.w),
// 右侧检查更新按钮 - 使用Expanded确保充分利用剩余空间
Expanded(
child: InkWell(
onTap: () {
// 调用检查更新方法
controller.checkForUpdate();
},
child: Container(
constraints: BoxConstraints(
minHeight: 36.h, // 设置最小高度而不是宽度约束
),
padding: EdgeInsets.symmetric(
horizontal: 12.w,
vertical: 8.h,
),
decoration: BoxDecoration(
color: isDarkMode
? Colors.blue[900]!.withOpacity(0.3)
: Colors.blue[100],
borderRadius: BorderRadius.circular(16.r),
),
child: Center(
child: FittedBox(
fit: BoxFit.scaleDown, // 如果文本过长,自动缩小字体
child: Text(
'checkUpdates'.tr, // 检查更新
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode
? Colors.blue[300]
: Colors.blue[600],
height: 1.2, // 稍微减少行高,适应FittedBox
),
textAlign: TextAlign.center,
maxLines: 2, // 最多显示2行,配合FittedBox使用
),
),
),
overflow: TextOverflow.ellipsis, // 添加省略号
maxLines: 1, // 限制为单行
textAlign: TextAlign.center, // 保持居中
),
),
),
@ -848,7 +813,7 @@ class SettingsView extends GetView<SettingsController> {
//------------测试使用标记 start---------------//
SizedBox(height: 4.h),
Text(
'${Platform.operatingSystem}-test-0707',
'${Platform.operatingSystem}-test-0721',
style: TextStyle(
fontSize: 12.sp,
color: const Color.fromARGB(255, 143, 94, 94),
@ -905,8 +870,10 @@ class SettingsView extends GetView<SettingsController> {
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.white : Colors.black87,
height: 1.2, // 设置行高,提升多行文本可读性
),
overflow: TextOverflow.ellipsis,
softWrap: true, // 启用软换行
overflow: TextOverflow.visible, // 确保文本完全显示
),
if (subtitle != null) SizedBox(height: 2.h),
if (subtitle != null)
@ -915,8 +882,10 @@ class SettingsView extends GetView<SettingsController> {
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
height: 1.3, // 设置行高,提升多行文本可读性
),
overflow: TextOverflow.ellipsis,
softWrap: true, // 启用软换行
overflow: TextOverflow.visible, // 确保文本完全显示
),
],
),
@ -980,8 +949,10 @@ class SettingsView extends GetView<SettingsController> {
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.white : Colors.black87,
height: 1.2, // 设置行高,提升多行文本可读性
),
overflow: TextOverflow.ellipsis,
softWrap: true, // 启用软换行
overflow: TextOverflow.visible, // 确保文本完全显示
),
if (subtitle != null) SizedBox(height: 2.h),
if (subtitle != null)
@ -991,8 +962,10 @@ class SettingsView extends GetView<SettingsController> {
fontSize: 12.sp,
color:
isDarkMode ? Colors.white70 : Colors.black54,
height: 1.3, // 设置行高,提升多行文本可读性
),
overflow: TextOverflow.ellipsis,
softWrap: true, // 启用软换行
overflow: TextOverflow.visible, // 确保文本完全显示
),
],
),
@ -1085,8 +1058,10 @@ class SettingsView extends GetView<SettingsController> {
fontWeight: FontWeight.w500,
color: titleColor ??
(isDarkMode ? Colors.white : Colors.black87),
height: 1.2, // 设置行高,提升多行文本可读性
),
overflow: TextOverflow.ellipsis,
softWrap: true, // 启用软换行
overflow: TextOverflow.visible, // 确保文本完全显示
),
if (subtitle != null) SizedBox(height: 2.h),
if (subtitle != null)
@ -1098,8 +1073,10 @@ class SettingsView extends GetView<SettingsController> {
(isDarkMode
? Colors.white70
: Colors.black54),
height: 1.3, // 设置行高,提升多行文本可读性
),
overflow: TextOverflow.ellipsis,
softWrap: true, // 启用软换行
overflow: TextOverflow.visible, // 确保文本完全显示
),
],
),

36
lib/modules/splash/controllers/splash_controller.dart

@ -76,7 +76,7 @@ class SplashController extends GetxController {
// await _initDatabase();
// 检查版本更新(在后台进行,不阻塞启动流程)
_checkVersionUpdate();
//_checkVersionUpdate();
_isResourceLoaded.value = true;
_checkNavigateCondition();
@ -159,21 +159,21 @@ class SplashController extends GetxController {
}
/// 检查版本更新(后台执行,不阻塞启动流程)
void _checkVersionUpdate() {
// 延迟执行版本检查,避免影响启动速度
Timer(const Duration(seconds: 1), () async {
try {
final versionService = VersionUpdateService.to;
final updateInfo = await versionService.checkForUpdate();
if (updateInfo.hasUpdate) {
// 显示更新对话框
await VersionUpdateDialog.show(updateInfo);
}
} catch (e) {
print('版本检查失败: $e');
// 版本检查失败不影响用户使用,静默处理
}
});
}
// void _checkVersionUpdate() {
// // 延迟执行版本检查,避免影响启动速度
// Timer(const Duration(seconds: 1), () async {
// try {
// final versionService = VersionUpdateService.to;
// final updateInfo = await versionService.checkForUpdate();
// if (updateInfo.hasUpdate) {
// // 显示更新对话框
// await VersionUpdateDialog.show(updateInfo);
// }
// } catch (e) {
// print('版本检查失败: $e');
// // 版本检查失败不影响用户使用,静默处理
// }
// });
// }
}

18
lib/modules/translation/bindings/translation_stats_binding.dart

@ -0,0 +1,18 @@
import 'package:get/get.dart';
import '../controllers/translation_stats_controller.dart';
import '../../../data/services/translation_usage_service.dart';
class TranslationStatsBinding implements Bindings {
@override
void dependencies() {
// 确保服务已注册
if (!Get.isRegistered<TranslationUsageService>()) {
Get.put(TranslationUsageService(), permanent: true);
}
// 注册控制器
Get.lazyPut<TranslationStatsController>(
() => TranslationStatsController(),
);
}
}

178
lib/modules/translation/controllers/translation_controller.dart

@ -14,8 +14,9 @@ import '../../../data/services/volcano_translation_service.dart';
import '../../../data/services/tts_service.dart';
import '../../../data/services/language_manager.dart';
import '../../../core/utils/logger.dart';
import '../../../data/models/events/voice_interaction_event.dart';
import '../../../data/services/asr_service.dart';
import '../../../data/services/translation_usage_service.dart';
import '../../../data/services/ble_manager.dart';
// 切换音频输出设备的枚举
@ -39,6 +40,15 @@ class TranslationController extends GetxController {
final bleManager = Get.find<BleManager>();
//音乐播放管理
final _musiceManager = Get.find<MusicManager>();
// 统计服务
late final TranslationUsageService _usageService;
// ASR统计相关 - 精确时长统计
DateTime? _asrSessionStartTime; // ASR会话开始时间
DateTime? _asrActiveStartTime; // ASR活跃开始时间(用户实际开始语音识别的时间)
Duration _asrActiveDuration = Duration.zero; // ASR累计活跃时长
bool _isAsrActive = false; // ASR是否处于活跃状态
// 音频输入源
bool _audioSourceType = false;
//是否开启播放
@ -121,15 +131,16 @@ class TranslationController extends GetxController {
// 监听识别结果
if (activeSpeaker.value != 0) {
isPlayback = false;
if (id == 1) {
await _asrService.restoreOriginalAudioState();
} else {
await _asrService.disableBluetoothAudio();
}
await _asrService.startContinuousRecognition(_audioSourceType);
// 开始ASR活跃时长跟踪(面对面模式)
_startAsrActiveTracking();
lastActiveSpeaker.value = id;
} else {
// 停止ASR活跃时长跟踪(面对面模式)
_stopAsrActiveTracking();
if (fTFTranslationResult != '' && !isPlayback) {
isPlayback = true;
print("松开走了着");
@ -178,6 +189,9 @@ class TranslationController extends GetxController {
super.onInit();
//_setupEventListeners();
// 初始化统计服务
_initUsageService();
// 加载上次保存的语言设置
_loadSavedLanguages();
@ -204,6 +218,20 @@ class TranslationController extends GetxController {
// 参考Agent模块的实现模式
}
// 初始化统计服务
void _initUsageService() {
try {
// 确保统计服务已注册
if (!Get.isRegistered<TranslationUsageService>()) {
Get.put(TranslationUsageService(), permanent: true);
}
_usageService = Get.find<TranslationUsageService>();
Logger.info('翻译统计服务初始化完成');
} catch (e) {
Logger.error('翻译统计服务初始化失败: $e');
}
}
// 加载保存的语言设置
void _loadSavedLanguages() {
final savedSourceLanguage = _storage.read(_sourceLanguageKey);
@ -230,9 +258,9 @@ class TranslationController extends GetxController {
try {
final bool granted = await PermissionUtil.instance.requestPermission(
permissionType: Permission.microphone,
permissionName: "录音",
explanationText: "我们需要访问麦克风权限,以便您录制音频内容。",
permanentDenialText: "检测到您已永久拒绝录音权限。如需继续使用此功能,请前往应用设置中手动开启麦克风权限。",
permissionName: 'microphonePermission'.tr,
explanationText: 'microphonePermissionExplanation'.tr,
permanentDenialText: 'microphonePermissionPermanentDenial'.tr,
);
if (granted) {
@ -254,9 +282,9 @@ class TranslationController extends GetxController {
try {
final bool granted = await PermissionUtil.instance.requestPermission(
permissionType: Permission.storage,
permissionName: "存储",
explanationText: "我们需要访问存储权限,以便您可以选择并上传音频文件。",
permanentDenialText: "检测到您已永久拒绝存储权限。如需继续使用此功能,请前往应用设置中手动开启存储权限。",
permissionName: 'storagePermission'.tr,
explanationText: 'storagePermissionExplanation'.tr,
permanentDenialText: 'storagePermissionPermanentDenial'.tr,
);
if (granted) {
@ -423,6 +451,17 @@ class TranslationController extends GetxController {
}
currentSessionId = DateTime.now().millisecondsSinceEpoch.toString();
// 开始新的翻译会话统计
try {
_usageService.startNewSession();
_asrSessionStartTime = DateTime.now(); // 记录ASR会话开始时间
_asrActiveDuration = Duration.zero; // 重置ASR活跃时长
_isAsrActive = false; // 重置ASR活跃状态
} catch (e) {
Logger.error('开始新会话统计失败: $e');
}
// 开始连续语音识别
if (currentMode.value == "audioVideo") {
bleManager.openDecoder();
@ -446,8 +485,9 @@ class TranslationController extends GetxController {
_asrService.stopContinuousRecognition();
} else {
await _ttsService.setAudioOutputDevice(0);
final recognitionStream =
await _asrService.startContinuousRecognition(_audioSourceType);
await _asrService.startContinuousRecognition(_audioSourceType);
// 开始ASR活跃时长计时
_startAsrActiveTracking();
}
isRecognizing.value = true;
if (isRecording.value && isRecognizing.value) {
@ -486,7 +526,10 @@ class TranslationController extends GetxController {
try {
await _asrService.stopContinuousRecognition();
await _asrService.restoreOriginalAudioState();
// 停止ASR活跃时长计时
_stopAsrActiveTracking();
isRecognizing.value = false;
// 如果当前有中间结果,将其作为最终结果处理
@ -564,6 +607,8 @@ class TranslationController extends GetxController {
Future<void> handleFinalResult(String text) async {
if (text.isEmpty || translationHistory.isEmpty) return;
// ASR识别到最终结果,表示用户完成了一次有效的语音输入
// 更新当前项
if (translationHistory.last.isIntermediate) {
translationHistory.last.sourceText = text;
@ -666,6 +711,33 @@ class TranslationController extends GetxController {
// 【改进】只有在最终结果时才保存,避免中间态时的频繁保存
if (isFinal) {
saveTranslationHistory();
// 记录翻译API调用统计 - 优化版(精确计费参数)
try {
final sourceLanguageName = _languageManager
.getChineseNameByAsrCode(detectedSourceLanguageCode) ??
'未知';
final targetLanguageName = _languageManager
.getChineseNameByAsrCode(detectedTargetLanguageCode) ??
'未知';
// 使用精确的计费参数:只统计源文本字符数(翻译API的实际计费基础)
_usageService.recordTranslationApiCall(
sourceText: sourceText, // 只有源文本用于翻译API计费
targetText: translationResult, // 目标文本用于TTS计费
sourceLanguage: sourceLanguageName,
targetLanguage: targetLanguageName,
mode: currentMode.value,
);
Logger.info(
'翻译统计: 源文本${sourceText.length}字符, 模式${currentMode.value}, 语言对$sourceLanguageName→$targetLanguageName');
// 记录ASR统计(在翻译完成时统计ASR使用情况)
_recordAsrUsageStats(sourceLanguageName);
} catch (e) {
Logger.error('记录翻译统计失败: $e');
}
}
// 更新后滚动到底部
@ -711,9 +783,32 @@ class TranslationController extends GetxController {
final voiceName = _languageManager
.getTtsVoiceNameByAsrCode(detectedTargetLanguageCode) ??
'en-US-AriaNeural';
print("voiceName=$voiceName,text$text");
Logger.info("voiceName=$voiceName,text=$text");
final startTime = DateTime.now();
await _ttsService.setVoice(voiceName);
await _ttsService.speakOnce(text);
final endTime = DateTime.now();
// 记录TTS API调用统计 - 优化版(基于文本字符数计费)
try {
final durationSeconds = endTime.difference(startTime).inSeconds;
final languageName = _languageManager
.getChineseNameByAsrCode(detectedTargetLanguageCode) ??
'未知';
// TTS的主要计费依据是文本字符数,播放时长仅用于分析
_usageService.recordTtsApiCall(
text: text, // 文本字符数是TTS计费的主要依据
durationSeconds: durationSeconds, // 播放时长用于用户体验分析
language: languageName,
);
Logger.info(
'TTS统计: 文本${text.length}字符, 播放${durationSeconds}秒, 语言$languageName');
} catch (e) {
Logger.error('记录TTS统计失败: $e');
}
} catch (e) {
Logger.error('播放翻译失败: ${e.toString()}');
}
@ -730,6 +825,8 @@ class TranslationController extends GetxController {
if (isRecognizing.value) {
await stopRecognition();
}
// 确保停止ASR活跃时长跟踪
_stopAsrActiveTracking();
stopRecording();
_translationDebounceTimer?.cancel();
await _ttsService.stop();
@ -1216,6 +1313,55 @@ class TranslationController extends GetxController {
void scrollToBottom({bool animate = true}) {
_scrollToBottom(animate: animate);
}
/// 开始ASR活跃时长跟踪
void _startAsrActiveTracking() {
if (!_isAsrActive) {
_asrActiveStartTime = DateTime.now();
_isAsrActive = true;
Logger.info('开始ASR活跃时长跟踪');
}
}
/// 停止ASR活跃时长跟踪
void _stopAsrActiveTracking() {
if (_isAsrActive && _asrActiveStartTime != null) {
final activeDuration = DateTime.now().difference(_asrActiveStartTime!);
_asrActiveDuration += activeDuration;
_isAsrActive = false;
_asrActiveStartTime = null;
Logger.info('停止ASR活跃时长跟踪,本次活跃时长: ${activeDuration.inSeconds}秒');
}
}
/// 记录ASR使用统计 - 基于精确时长跟踪
void _recordAsrUsageStats(String language) {
try {
if (_asrSessionStartTime == null) return;
// 确保停止当前的活跃时长跟踪
_stopAsrActiveTracking();
final now = DateTime.now();
final totalDurationSeconds =
now.difference(_asrSessionStartTime!).inSeconds;
final effectiveDurationSeconds = _asrActiveDuration.inSeconds;
_usageService.recordAsrApiCall(
effectiveDurationSeconds: effectiveDurationSeconds,
totalDurationSeconds: totalDurationSeconds,
language: language,
mode: currentMode.value,
audioFormat: 'pcm',
sampleRate: 16000,
);
Logger.info(
'ASR会话统计: 总时长${totalDurationSeconds}秒, 实际活跃时长${effectiveDurationSeconds}秒, 语言$language');
} catch (e) {
Logger.error('记录ASR统计失败: $e');
}
}
}
// 翻译项模型

258
lib/modules/translation/controllers/translation_cost_controller.dart

@ -0,0 +1,258 @@
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import '../../../data/services/translation_usage_service.dart';
import '../../../core/utils/logger.dart';
/// 翻译成本配置控制器
class TranslationCostController extends GetxController {
// 依赖注入
final TranslationUsageService _usageService = Get.find<TranslationUsageService>();
// 成本配置
final RxMap<String, double> costConfig = <String, double>{}.obs;
final RxDouble dailyCostThreshold = 10.0.obs;
final RxDouble monthlyCostThreshold = 300.0.obs;
// 表单控制器
final translationCostController = TextEditingController();
final asrCostController = TextEditingController();
final ttsCostController = TextEditingController();
final aiSummaryCostController = TextEditingController();
final dailyThresholdController = TextEditingController();
final monthlyThresholdController = TextEditingController();
// 状态变量
final isLoading = false.obs;
final isSaving = false.obs;
@override
void onInit() {
super.onInit();
_loadCurrentConfig();
}
@override
void onClose() {
// 释放控制器
translationCostController.dispose();
asrCostController.dispose();
ttsCostController.dispose();
aiSummaryCostController.dispose();
dailyThresholdController.dispose();
monthlyThresholdController.dispose();
super.onClose();
}
/// 加载当前配置
void _loadCurrentConfig() {
isLoading.value = true;
try {
// 从服务获取当前配置
costConfig.value = Map<String, double>.from(_usageService.costConfig);
dailyCostThreshold.value = _usageService.dailyCostThreshold.value;
monthlyCostThreshold.value = _usageService.monthlyCostThreshold.value;
// 更新表单控制器
translationCostController.text = (costConfig['translation_per_1k_chars'] ?? 0.05).toString();
asrCostController.text = (costConfig['asr_per_minute'] ?? 0.02).toString();
ttsCostController.text = (costConfig['tts_per_1k_chars'] ?? 0.03).toString();
aiSummaryCostController.text = (costConfig['ai_summary_per_request'] ?? 0.01).toString();
dailyThresholdController.text = dailyCostThreshold.value.toString();
monthlyThresholdController.text = monthlyCostThreshold.value.toString();
Logger.info('成本配置加载完成');
} catch (e) {
Logger.error('加载成本配置失败: $e');
_showErrorSnackbar('加载配置失败');
} finally {
isLoading.value = false;
}
}
/// 保存配置
Future<void> saveConfig() async {
if (isSaving.value) return;
isSaving.value = true;
try {
// 验证输入
if (!_validateInputs()) {
return;
}
// 更新成本配置
final newCostConfig = {
'translation_per_1k_chars': double.parse(translationCostController.text),
'asr_per_minute': double.parse(asrCostController.text),
'tts_per_1k_chars': double.parse(ttsCostController.text),
'ai_summary_per_request': double.parse(aiSummaryCostController.text),
};
final newDailyThreshold = double.parse(dailyThresholdController.text);
final newMonthlyThreshold = double.parse(monthlyThresholdController.text);
// 保存到服务
_usageService.updateCostConfig(newCostConfig);
_usageService.updateThresholds(
dailyThreshold: newDailyThreshold,
monthlyThreshold: newMonthlyThreshold,
);
// 更新本地状态
costConfig.value = newCostConfig;
dailyCostThreshold.value = newDailyThreshold;
monthlyCostThreshold.value = newMonthlyThreshold;
_showSuccessSnackbar('配置保存成功');
Logger.info('成本配置保存成功');
} catch (e) {
Logger.error('保存成本配置失败: $e');
_showErrorSnackbar('保存配置失败');
} finally {
isSaving.value = false;
}
}
/// 重置为默认配置
void resetToDefault() {
translationCostController.text = '0.05';
asrCostController.text = '0.02';
ttsCostController.text = '0.03';
aiSummaryCostController.text = '0.01';
dailyThresholdController.text = '10.0';
monthlyThresholdController.text = '300.0';
_showInfoSnackbar('已重置为默认配置,请点击保存生效');
}
/// 验证输入
bool _validateInputs() {
try {
final translationCost = double.parse(translationCostController.text);
final asrCost = double.parse(asrCostController.text);
final ttsCost = double.parse(ttsCostController.text);
final aiSummaryCost = double.parse(aiSummaryCostController.text);
final dailyThreshold = double.parse(dailyThresholdController.text);
final monthlyThreshold = double.parse(monthlyThresholdController.text);
if (translationCost < 0 || asrCost < 0 || ttsCost < 0 || aiSummaryCost < 0) {
_showErrorSnackbar('成本不能为负数');
return false;
}
if (dailyThreshold < 0 || monthlyThreshold < 0) {
_showErrorSnackbar('预警阈值不能为负数');
return false;
}
if (dailyThreshold > monthlyThreshold) {
_showErrorSnackbar('日预警阈值不能大于月预警阈值');
return false;
}
return true;
} catch (e) {
_showErrorSnackbar('请输入有效的数字');
return false;
}
}
/// 计算预估月成本
double calculateEstimatedMonthlyCost() {
try {
final dailyStats = _usageService.todayStats.value;
if (dailyStats.estimatedCost == 0) return 0.0;
// 简单按30天计算
return dailyStats.estimatedCost * 30;
} catch (e) {
Logger.error('计算预估月成本失败: $e');
return 0.0;
}
}
/// 获取成本节约建议
List<String> getCostSavingTips() {
final tips = <String>[];
try {
final dailyStats = _usageService.todayStats.value;
// 基于使用情况给出建议
if (dailyStats.translationApiCalls > 100) {
tips.add('考虑批量翻译以减少API调用次数');
}
if (dailyStats.ttsApiCalls > 50) {
tips.add('可以关闭自动语音播放以节省TTS成本');
}
if (dailyStats.aiSummaryApiCalls > 10) {
tips.add('AI摘要功能按需使用,避免频繁调用');
}
if (dailyStats.effectiveAudioDuration > 3600) { // 超过1小时
tips.add('长时间录音会增加ASR成本,建议分段录音');
}
if (tips.isEmpty) {
tips.add('您的使用习惯很好,继续保持!');
}
} catch (e) {
Logger.error('获取成本节约建议失败: $e');
tips.add('暂无建议');
}
return tips;
}
/// 显示成功提示
void _showSuccessSnackbar(String message) {
Get.snackbar(
'成功',
message,
snackPosition: SnackPosition.TOP,
backgroundColor: Get.theme.colorScheme.primary.withOpacity(0.1),
colorText: Get.theme.colorScheme.primary,
duration: const Duration(seconds: 3),
);
}
/// 显示错误提示
void _showErrorSnackbar(String message) {
Get.snackbar(
'错误',
message,
snackPosition: SnackPosition.TOP,
backgroundColor: Get.theme.colorScheme.error.withOpacity(0.1),
colorText: Get.theme.colorScheme.error,
duration: const Duration(seconds: 3),
);
}
/// 显示信息提示
void _showInfoSnackbar(String message) {
Get.snackbar(
'提示',
message,
snackPosition: SnackPosition.TOP,
backgroundColor: Get.theme.colorScheme.surface.withOpacity(0.9),
colorText: Get.theme.colorScheme.onSurface,
duration: const Duration(seconds: 3),
);
}
/// 格式化成本显示
String formatCost(double cost) {
return '¥${cost.toStringAsFixed(2)}';
}
/// 格式化百分比显示
String formatPercentage(double percentage) {
return '${percentage.toStringAsFixed(1)}%';
}
}

25
lib/modules/translation/controllers/translation_detail_controller.dart

@ -6,11 +6,13 @@ import '../controllers/translation_history_controller.dart';
import 'package:get_storage/get_storage.dart';
import 'dart:async';
import '../../../core/utils/logger.dart';
import '../../../data/services/translation_usage_service.dart';
class TranslationDetailController extends GetxController {
// 依赖注入
final VolcanoAIService _aiService = Get.find<VolcanoAIService>();
final GetStorage _storage = GetStorage();
late final TranslationUsageService _usageService;
// 存储键前缀
static const String _summaryKeyPrefix = 'translation_summary_';
@ -33,13 +35,23 @@ class TranslationDetailController extends GetxController {
@override
void onInit() {
super.onInit();
// 初始化统计服务
try {
if (!Get.isRegistered<TranslationUsageService>()) {
Get.put(TranslationUsageService(), permanent: true);
}
_usageService = Get.find<TranslationUsageService>();
} catch (e) {
Logger.error('初始化统计服务失败: $e');
}
// 从路由参数中获取数据
if (Get.arguments != null) {
currentItem = Get.arguments['item'];
sessionItems = Get.arguments['sessionItems'];
historyController = Get.arguments['historyController'];
// 异步加载数据,避免阻塞主线程
_asyncInitialize();
} else {
@ -170,8 +182,15 @@ class TranslationDetailController extends GetxController {
if (result.isNotEmpty) {
summaryResult.value = result;
saveSummary(result);
// 记录AI摘要API调用统计
try {
_usageService.recordAiSummaryApiCall();
} catch (e) {
Logger.error('记录AI摘要统计失败: $e');
}
}
return result;
} catch (e) {
final errorMessage = '生成摘要失败: ${e.toString()}';

358
lib/modules/translation/controllers/translation_stats_controller.dart

@ -0,0 +1,358 @@
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import '../../../data/services/translation_usage_service.dart';
import '../../../data/models/translation_usage_stats.dart';
import '../../../core/utils/logger.dart';
/// 翻译统计页面控制器
class TranslationStatsController extends GetxController {
// 依赖注入
final TranslationUsageService _usageService =
Get.find<TranslationUsageService>();
// 状态变量
final isLoading = false.obs;
final selectedTabIndex = 0.obs; // 0=今日, 1=本周, 2=本月, 3=历史
final selectedTimeRange = '今日'.obs;
// 统计数据
final Rx<TranslationUsageStats> todayStats =
TranslationUsageStats.empty('').obs;
final Rx<TranslationUsageStats> weekStats =
TranslationUsageStats.empty('').obs;
final Rx<TranslationUsageStats> monthStats =
TranslationUsageStats.empty('').obs;
final RxList<TranslationUsageStats> historyStats =
<TranslationUsageStats>[].obs;
// 实时统计数据
RealtimeUsageStats get realtimeStats => _usageService.realtimeStats.value;
// 成本配置
final RxMap<String, double> costConfig = <String, double>{}.obs;
final RxDouble dailyCostThreshold = 10.0.obs;
final RxDouble monthlyCostThreshold = 300.0.obs;
// 图表数据
final RxList<Map<String, dynamic>> chartData = <Map<String, dynamic>>[].obs;
@override
void onInit() {
super.onInit();
_initializeData();
}
/// 初始化数据
void _initializeData() {
// 监听使用服务的数据变化
ever(_usageService.todayStats, (stats) {
todayStats.value = stats;
_updateChartData();
});
ever(_usageService.costConfig, (config) {
costConfig.value = config;
});
ever(_usageService.dailyCostThreshold, (threshold) {
dailyCostThreshold.value = threshold;
});
ever(_usageService.monthlyCostThreshold, (threshold) {
monthlyCostThreshold.value = threshold;
});
// 初始加载数据
loadAllStats();
}
/// 加载所有统计数据
Future<void> loadAllStats() async {
isLoading.value = true;
try {
// 加载今日数据
todayStats.value = _usageService.todayStats.value;
// 加载本周数据
await _loadWeekStats();
// 加载本月数据
await _loadMonthStats();
// 加载历史数据(最近7天)
await _loadHistoryStats();
// 更新图表数据
_updateChartData();
Logger.info('统计数据加载完成');
} catch (e) {
Logger.error('加载统计数据失败: $e');
Get.snackbar(
'加载失败',
'统计数据加载失败,请稍后重试',
snackPosition: SnackPosition.TOP,
backgroundColor: Get.theme.colorScheme.error.withOpacity(0.1),
colorText: Get.theme.colorScheme.error,
);
} finally {
isLoading.value = false;
}
}
/// 加载本周统计数据
Future<void> _loadWeekStats() async {
try {
final now = DateTime.now();
final weekStart = now.subtract(Duration(days: now.weekday - 1));
final weekEnd = weekStart.add(const Duration(days: 6));
final dailyStats = await _usageService.getHistoryStats(
startDate: weekStart,
endDate: weekEnd,
);
weekStats.value = _aggregateStats(dailyStats, 'week');
} catch (e) {
Logger.error('加载本周统计失败: $e');
}
}
/// 加载本月统计数据
Future<void> _loadMonthStats() async {
try {
final now = DateTime.now();
monthStats.value =
await _usageService.getMonthlyStats(now.year, now.month);
} catch (e) {
Logger.error('加载本月统计失败: $e');
}
}
/// 加载历史统计数据
Future<void> _loadHistoryStats() async {
try {
final endDate = DateTime.now();
final startDate = endDate.subtract(const Duration(days: 7));
historyStats.value = await _usageService.getHistoryStats(
startDate: startDate,
endDate: endDate,
);
// 按日期排序
historyStats.sort((a, b) => b.date.compareTo(a.date));
} catch (e) {
Logger.error('加载历史统计失败: $e');
}
}
/// 聚合统计数据
TranslationUsageStats _aggregateStats(
List<TranslationUsageStats> statsList, String period) {
if (statsList.isEmpty) {
return TranslationUsageStats.empty(_usageService.getCurrentUserId());
}
int totalTranslationCalls = 0;
int totalAsrCalls = 0;
int totalTtsCalls = 0;
int totalAiSummaryCalls = 0;
int totalSourceChars = 0;
int totalTargetChars = 0;
int totalRecordingDuration = 0;
int totalTtsPlayDuration = 0;
int totalSessions = 0;
double totalCost = 0.0;
final Map<String, int> combinedLanguagePairUsage = {};
final Map<String, int> combinedModeUsage = {};
for (final stats in statsList) {
totalTranslationCalls += stats.translationApiCalls;
totalAsrCalls += stats.asrApiCalls;
totalTtsCalls += stats.ttsApiCalls;
totalAiSummaryCalls += stats.aiSummaryApiCalls;
totalSourceChars += stats.sourceTextCharacters;
totalTargetChars += stats.ttsTextCharacters;
totalRecordingDuration += stats.effectiveAudioDuration;
totalTtsPlayDuration += stats.totalSessionDuration;
totalSessions += stats.sessionCount;
totalCost += stats.estimatedCost;
// 合并语言对使用统计(使用兼容性getter)
stats.languagePairUsage.forEach((key, value) {
combinedLanguagePairUsage[key] =
(combinedLanguagePairUsage[key] ?? 0) + value;
});
// 合并模式使用统计(使用兼容性getter)
stats.modeUsage.forEach((key, value) {
combinedModeUsage[key] = (combinedModeUsage[key] ?? 0) + value;
});
}
return TranslationUsageStats(
id: '${period}_${DateTime.now().millisecondsSinceEpoch}',
date: DateTime.now(),
userId: 'current_user',
timeGranularity: period,
translationApiCalls: totalTranslationCalls,
asrApiCalls: totalAsrCalls,
ttsApiCalls: totalTtsCalls,
aiSummaryApiCalls: totalAiSummaryCalls,
sourceTextCharacters: totalSourceChars,
ttsTextCharacters: totalTargetChars,
effectiveAudioDuration: totalRecordingDuration,
totalSessionDuration: totalTtsPlayDuration,
sessionCount: totalSessions,
estimatedCost: totalCost,
);
}
/// 更新图表数据
void _updateChartData() {
final List<Map<String, dynamic>> data = [];
// 根据选中的时间范围生成图表数据
switch (selectedTabIndex.value) {
case 0: // 今日
data.add({
'label': '今日',
'apiCalls': todayStats.value.totalApiCalls,
'cost': todayStats.value.estimatedCost,
'characters': todayStats.value.totalCharacterCount,
});
break;
case 1: // 本周
data.add({
'label': '本周',
'apiCalls': weekStats.value.totalApiCalls,
'cost': weekStats.value.estimatedCost,
'characters': weekStats.value.totalCharacterCount,
});
break;
case 2: // 本月
data.add({
'label': '本月',
'apiCalls': monthStats.value.totalApiCalls,
'cost': monthStats.value.estimatedCost,
'characters': monthStats.value.totalCharacterCount,
});
break;
case 3: // 历史
for (final stats in historyStats.take(7)) {
data.add({
'label': '${stats.date.month}/${stats.date.day}',
'apiCalls': stats.totalApiCalls,
'cost': stats.estimatedCost,
'characters': stats.totalCharacterCount,
});
}
break;
}
chartData.value = data;
}
/// 切换时间范围
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;
}
_updateChartData();
}
/// 获取当前显示的统计数据
TranslationUsageStats get currentStats {
switch (selectedTabIndex.value) {
case 0:
return todayStats.value;
case 1:
return weekStats.value;
case 2:
return monthStats.value;
default:
return todayStats.value;
}
}
/// 刷新数据
Future<void> refreshData() async {
await loadAllStats();
}
/// 清理过期数据
void cleanupOldData() {
_usageService.cleanupOldData();
Get.snackbar(
'清理完成',
'已清理90天前的过期数据',
snackPosition: SnackPosition.TOP,
backgroundColor: Get.theme.colorScheme.primary.withOpacity(0.1),
colorText: Get.theme.colorScheme.primary,
);
}
/// 导出统计数据
Future<void> exportStats() async {
try {
// TODO: 实现数据导出功能
Get.snackbar(
'导出功能',
'数据导出功能正在开发中',
snackPosition: SnackPosition.TOP,
backgroundColor: Get.theme.colorScheme.primary.withOpacity(0.1),
colorText: Get.theme.colorScheme.primary,
);
} catch (e) {
Logger.error('导出统计数据失败: $e');
}
}
/// 格式化时长显示
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 formatCost(double cost) {
return '¥${cost.toStringAsFixed(2)}';
}
/// 格式化数字显示
String formatNumber(int number) {
if (number < 1000) {
return number.toString();
} else if (number < 1000000) {
return '${(number / 1000).toStringAsFixed(1)}K';
} else {
return '${(number / 1000000).toStringAsFixed(1)}M';
}
}
}

2
lib/modules/translation/views/translation_detail_view.dart

@ -110,7 +110,7 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
isDarkMode: isDarkMode,
)),
),
//屏蔽“记录摘要”,需要是开启
//屏蔽“记录摘要”,需要时开启
// SizedBox(width: 12.w),
// Expanded(
// child: Obx(() => _buildTabButton(

757
lib/modules/translation/views/translation_stats_view.dart

@ -0,0 +1,757 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../controllers/translation_stats_controller.dart';
import '../../../core/theme/app_colors.dart';
/// 翻译统计页面
class TranslationStatsView extends GetView<TranslationStatsController> {
const TranslationStatsView({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
? _buildLoadingWidget()
: _buildBody(isDarkMode)),
);
}
/// 构建应用栏
PreferredSizeWidget _buildAppBar(bool isDarkMode) {
return AppBar(
backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.white,
elevation: 0.5,
title: Text(
'翻译统计', // translationStats
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,
),
// 更多操作菜单
PopupMenuButton<String>(
icon: Icon(
Icons.more_vert,
color: isDarkMode ? Colors.white : Colors.black,
),
onSelected: (value) {
switch (value) {
case 'cleanup':
controller.cleanupOldData();
break;
case 'export':
controller.exportStats();
break;
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'cleanup',
child: Text('清理过期数据'),
),
const PopupMenuItem(
value: 'export',
child: Text('导出数据'),
),
],
),
],
);
}
/// 构建加载组件
Widget _buildLoadingWidget() {
return const Center(
child: CircularProgressIndicator(),
);
}
/// 构建主体内容
Widget _buildBody(bool isDarkMode) {
return Column(
children: [
// 时间范围选择器
_buildTimeRangeSelector(isDarkMode),
// 统计内容
Expanded(
child: SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: Column(
children: [
// 实时统计卡片
if (controller.selectedTabIndex.value == 0)
_buildRealtimeStatsCard(isDarkMode),
SizedBox(height: 16.h),
// 概览统计卡片
_buildOverviewCard(isDarkMode),
SizedBox(height: 16.h),
// API调用统计
_buildApiCallsCard(isDarkMode),
SizedBox(height: 16.h),
// 数据量统计
_buildDataVolumeCard(isDarkMode),
SizedBox(height: 16.h),
// 成本统计
_buildCostCard(isDarkMode),
SizedBox(height: 16.h),
// 使用行为统计
_buildUsageBehaviorCard(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.withOpacity(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
? (isDarkMode ? AppColors.primary : 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 _buildRealtimeStatsCard(bool isDarkMode) {
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.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.access_time,
color: AppColors.primary,
size: 20.sp,
),
SizedBox(width: 8.w),
Text(
'当前会话统计',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
],
),
SizedBox(height: 16.h),
Obx(() => Row(
children: [
Expanded(
child: _buildStatItem(
'翻译次数',
controller.realtimeStats.translationCount.toString(),
Icons.translate,
isDarkMode,
),
),
Expanded(
child: _buildStatItem(
'会话时长',
'${controller.realtimeStats.sessionDurationMinutes}分钟',
Icons.timer,
isDarkMode,
),
),
],
)),
SizedBox(height: 12.h),
Obx(() => Row(
children: [
Expanded(
child: _buildStatItem(
'字符数',
controller.formatNumber(
controller.realtimeStats.sourceCharacters +
controller.realtimeStats.targetCharacters,
),
Icons.text_fields,
isDarkMode,
),
),
Expanded(
child: _buildStatItem(
'录音时长',
controller.formatDuration(controller.realtimeStats.recordingDuration),
Icons.mic,
isDarkMode,
),
),
],
)),
],
),
);
}
/// 构建概览卡片
Widget _buildOverviewCard(bool isDarkMode) {
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.withOpacity(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),
Obx(() => Row(
children: [
Expanded(
child: _buildStatItem(
'总调用次数',
controller.formatNumber(controller.currentStats.totalApiCalls),
Icons.api,
isDarkMode,
),
),
Expanded(
child: _buildStatItem(
'预估成本',
controller.formatCost(controller.currentStats.estimatedCost),
Icons.attach_money,
isDarkMode,
),
),
],
)),
SizedBox(height: 12.h),
Obx(() => Row(
children: [
Expanded(
child: _buildStatItem(
'会话数量',
controller.currentStats.sessionCount.toString(),
Icons.chat,
isDarkMode,
),
),
Expanded(
child: _buildStatItem(
'总字符数',
controller.formatNumber(controller.currentStats.totalCharacterCount),
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,
),
),
],
);
}
/// 构建API调用统计卡片
Widget _buildApiCallsCard(bool isDarkMode) {
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.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.api,
color: AppColors.primary,
size: 20.sp,
),
SizedBox(width: 8.w),
Text(
'API调用统计',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
],
),
SizedBox(height: 16.h),
Obx(() => Column(
children: [
_buildApiCallRow('翻译API', controller.currentStats.translationApiCalls, Icons.translate, isDarkMode),
SizedBox(height: 12.h),
_buildApiCallRow('语音识别', controller.currentStats.asrApiCalls, Icons.mic, isDarkMode),
SizedBox(height: 12.h),
_buildApiCallRow('语音合成', controller.currentStats.ttsApiCalls, Icons.volume_up, isDarkMode),
SizedBox(height: 12.h),
_buildApiCallRow('AI摘要', controller.currentStats.aiSummaryApiCalls, Icons.summarize, isDarkMode),
],
)),
],
),
);
}
/// 构建API调用行
Widget _buildApiCallRow(String label, int count, IconData icon, bool isDarkMode) {
return Row(
children: [
Icon(
icon,
color: AppColors.primary,
size: 18.sp,
),
SizedBox(width: 12.w),
Expanded(
child: Text(
label,
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black,
),
),
),
Text(
controller.formatNumber(count),
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
],
);
}
/// 构建数据量统计卡片
Widget _buildDataVolumeCard(bool isDarkMode) {
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.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.data_usage,
color: AppColors.primary,
size: 20.sp,
),
SizedBox(width: 8.w),
Text(
'数据量统计',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
],
),
SizedBox(height: 16.h),
Obx(() => Column(
children: [
Row(
children: [
Expanded(
child: _buildStatItem(
'源文本字符',
controller.formatNumber(controller.currentStats.sourceTextCharacters),
Icons.text_fields,
isDarkMode,
),
),
Expanded(
child: _buildStatItem(
'TTS文本字符',
controller.formatNumber(controller.currentStats.ttsTextCharacters),
Icons.translate,
isDarkMode,
),
),
],
),
SizedBox(height: 16.h),
Row(
children: [
Expanded(
child: _buildStatItem(
'有效音频时长',
controller.formatDuration(controller.currentStats.effectiveAudioDuration),
Icons.mic,
isDarkMode,
),
),
Expanded(
child: _buildStatItem(
'总会话时长',
controller.formatDuration(controller.currentStats.totalSessionDuration),
Icons.volume_up,
isDarkMode,
),
),
],
),
],
)),
],
),
);
}
/// 构建成本统计卡片
Widget _buildCostCard(bool isDarkMode) {
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.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.attach_money,
color: AppColors.primary,
size: 20.sp,
),
SizedBox(width: 8.w),
Text(
'成本统计',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
],
),
SizedBox(height: 16.h),
Obx(() => Column(
children: [
// 当前成本
Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: AppColors.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(8.r),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${controller.selectedTimeRange.value}预估成本',
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.white : Colors.black,
),
),
Text(
controller.formatCost(controller.currentStats.estimatedCost),
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
color: AppColors.primary,
),
),
],
),
),
SizedBox(height: 12.h),
// 预警阈值
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'日成本预警阈值',
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
Text(
controller.formatCost(controller.dailyCostThreshold.value),
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
],
),
SizedBox(height: 4.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'月成本预警阈值',
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
Text(
controller.formatCost(controller.monthlyCostThreshold.value),
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
],
),
],
)),
],
),
);
}
/// 构建使用行为统计卡片
Widget _buildUsageBehaviorCard(bool isDarkMode) {
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.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.analytics,
color: AppColors.primary,
size: 20.sp,
),
SizedBox(width: 8.w),
Text(
'使用行为统计',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
],
),
SizedBox(height: 16.h),
Obx(() => Column(
children: [
// 最常用语言对
_buildUsageBehaviorRow(
'最常用语言对',
controller.currentStats.mostUsedLanguagePair,
Icons.language,
isDarkMode,
),
SizedBox(height: 12.h),
// 最常用翻译模式
_buildUsageBehaviorRow(
'最常用模式',
controller.currentStats.mostUsedMode,
Icons.settings,
isDarkMode,
),
],
)),
],
),
);
}
/// 构建使用行为行
Widget _buildUsageBehaviorRow(String label, String value, IconData icon, bool isDarkMode) {
return Row(
children: [
Icon(
icon,
color: AppColors.primary,
size: 18.sp,
),
SizedBox(width: 12.w),
Expanded(
child: Text(
label,
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black,
),
),
),
Text(
value,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
],
);
}
}

158
lib/modules/translation/views/translation_view.dart

@ -664,32 +664,35 @@ class TranslationView extends GetView<TranslationController> {
void _showLanguageSelectionDialog(bool isSource, bool isDarkMode) {
Get.dialog(
Dialog(
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24.r),
borderRadius: BorderRadius.circular(16.r), // 统一圆角尺寸为16
),
insetPadding: EdgeInsets.symmetric(horizontal: 20.w),
backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white,
child: Container(
width: 0.9.sw,
padding: EdgeInsets.only(top: 32.h, bottom: 16.h),
child: Padding(
// 使用Padding替代Container的padding
padding: EdgeInsets.all(12.w), // 统一内边距
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题区域
Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
padding: EdgeInsets.symmetric(horizontal: 8.w),
child: Text(
isSource
? 'selectSourceLanguage'.tr // 选择源语言
: 'selectTargetLanguage'.tr, // 选择目标语言
? 'selectSourceLanguage'.tr
: 'selectTargetLanguage'.tr, // 保留源语言/目标语言区分
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black,
fontSize: 18.sp, // 调整字号
fontWeight: FontWeight.w600, // 使用数字字重
color: isDarkMode ? Colors.white : Colors.black87, // 优化文本颜色
),
),
),
SizedBox(height: 16.h),
// 语言列表区域
Container(
constraints: BoxConstraints(maxHeight: 0.5.sh),
child: SingleChildScrollView(
@ -697,52 +700,105 @@ class TranslationView extends GetView<TranslationController> {
mainAxisSize: MainAxisSize.min,
children:
controller.supportedLanguages.keys.map((language) {
return ListTile(
title: Text(
'${language}'.tr,
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black,
return Obx(() {
final currentLanguage = isSource
? controller.sourceLanguage.value
: controller.targetLanguage.value;
final isSelected = currentLanguage == language;
return GestureDetector(
// 改用手势检测器实现点击
onTap: () {
if (isSource) {
controller.setSourceLanguage(language);
} else {
controller.setTargetLanguage(language);
}
Get.back();
},
child: Container(
margin: EdgeInsets.only(bottom: 10.h), // 添加项间距
padding: EdgeInsets.all(10.w), // 统一内边距
decoration: BoxDecoration(
color: isSelected // 选中状态背景色
? (isDarkMode
? const Color(0xFF6200EE).withOpacity(0.2)
: Colors.blue.withOpacity(0.1))
: (isDarkMode // 未选中状态背景色
? Colors.white.withOpacity(0.05)
: Colors.grey[200]!),
borderRadius: BorderRadius.circular(12.r), // 项圆角
border: Border.all(
// 选中状态边框
color: isSelected
? (isDarkMode
? const Color(0xFF6200EE)
: Colors.blue)
: (isDarkMode // 未选中状态背景色
? Colors.white.withOpacity(0.05)
: Colors.grey[200]!),
width: 1,
),
),
child: Row(
children: [
// 语言名称
Expanded(
child: Text(
language.tr,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: isDarkMode
? Colors.white
: Colors.black87,
overflow: TextOverflow.ellipsis,
),
),
),
// 选中指示器
Container(
width: 24.w,
height: 24.w,
decoration: BoxDecoration(
color: isSelected // 动态颜色
? (isDarkMode
? const Color(0xFF6200EE)
: Colors.blue)
: (isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[300]),
shape: BoxShape.circle, // 圆形样式
),
child: isSelected
? Icon(
// 选中时显示对勾
Icons.check,
size: 16.w,
color: Colors.white,
)
: null,
),
],
),
),
),
trailing: Obx(() {
final currentLanguage = isSource
? controller.sourceLanguage.value
: controller.targetLanguage.value;
return currentLanguage == language
? Icon(
Icons.check_circle,
color: isDarkMode
? AppColors.primary
: Colors.blue,
)
: const SizedBox.shrink();
}),
onTap: () {
if (isSource) {
controller.setSourceLanguage(language);
} else {
controller.setTargetLanguage(language);
}
Get.back();
},
);
);
});
}).toList(),
),
),
),
SizedBox(height: 16.h),
// 取消按钮
Align(
alignment: Alignment.centerRight,
child: Padding(
padding: EdgeInsets.only(right: 24.w),
child: TextButton(
onPressed: () => Get.back(),
child: Text(
'cancel'.tr, // 取消
style: TextStyle(
color: isDarkMode ? AppColors.primary : Colors.blue,
fontSize: 16.sp,
),
child: TextButton(
onPressed: () => Get.back(),
child: Text(
'cancel'.tr,
style: TextStyle(
color: isDarkMode ? const Color(0xFF6200EE) : Colors.blue,
),
),
),
@ -974,7 +1030,7 @@ class TranslationView extends GetView<TranslationController> {
// 显示删除成功提示
Get.snackbar(
'删除成功', // deleteSuccess
'success'.tr, // deleteSuccess
'已删除该翻译记录', // deleteTranslationSuccess
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Get.isDarkMode ? Colors.grey[800] : Colors.grey[200],
@ -984,7 +1040,7 @@ class TranslationView extends GetView<TranslationController> {
} catch (e) {
// 显示删除失败提示
Get.snackbar(
'删除失败', // deleteFailed
'error'.tr, // deleteFailed
'删除翻译记录时发生错误: ${e.toString()}', // deleteTranslationError
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red[400],

17
lib/routes/app_pages.dart

@ -21,6 +21,7 @@ import '../modules/meeting/views/meeting_view.dart';
import '../modules/opus_test/bindings/opus_test_binding.dart';
import '../modules/opus_test/views/opus_test_view.dart';
import '../modules/profile/views/profile_view.dart';
import '../modules/profile/views/profile_meeting_view.dart';
import '../modules/profile/bindings/profile_binding.dart';
// import '../modules/chat/bindings/chat_binding.dart';
//import '../modules/chat/views/chat_view.dart';
@ -30,6 +31,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/translation/views/translation_stats_view.dart';
import '../modules/translation/bindings/translation_stats_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';
@ -129,6 +132,13 @@ abstract class AppPages {
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage(
name: Routes.profileMeeting,
page: () => ProfileMeetingView(),
binding: ProfileBinding(),
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage(
name: Routes.translation,
page: () => const TranslationView(),
@ -143,6 +153,13 @@ abstract class AppPages {
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage(
name: Routes.translationStats,
page: () => const TranslationStatsView(),
binding: TranslationStatsBinding(),
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage(
name: Routes.translationList,
page: () => const TranslationListView(),

4
lib/routes/app_routes.dart

@ -1,6 +1,6 @@
abstract class Routes {
// 认证相关
static const splash = '/';
static const splash = '/splash';
static const login = '/login';
static const pairing = '/pairing';
@ -11,6 +11,7 @@ abstract class Routes {
static const chat = '/chat';
static const profile = '/profile';
static const editProfile = '/edit_profile';
static const profileMeeting = '/profile_meeting'; // 个人信息页面(从会议模块跳转)
// 设备管理
static const OTA_UPGRADE = '/ota_upgrade'; // OTA升级
@ -27,6 +28,7 @@ abstract class Routes {
// 语言功能
static const translation = '/translation'; // 实时翻译
static const translationHistory = '/translation/history'; // 翻译历史
static const translationStats = '/translation/stats'; // 翻译统计
static const speechDemo = '/speech_demo'; // 语音演示
static const translationList = '/translation/List'; // 翻译历史
static const ftftranslation = '/ftftranslation'; // 面对面翻译

64
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt

@ -52,6 +52,9 @@ interface AgentServiceListener {
object AgentService : CoroutineScope {
private val TAG = "AgentService"
private var azureSpeechKey: String = ""
private var azureSpeechRegion: String = ""
// 协程相关
private val job = SupervisorJob()
override val coroutineContext: CoroutineContext
@ -179,6 +182,9 @@ object AgentService : CoroutineScope {
LocationService.initialize(appContext)
// 设置系统提示词
val customSystemPrompt = config["systemPrompt"] as? String
//保存配置
this.azureSpeechKey = config["azureSpeechKey"]?.toString() ?: ""
this.azureSpeechRegion = config["azureSpeechRegion"]?.toString() ?: ""
if (!customSystemPrompt.isNullOrBlank()) {
systemPrompt = customSystemPrompt
} else {
@ -188,9 +194,15 @@ object AgentService : CoroutineScope {
azureAsrHelper = AzureAsrHelper(context).apply {
// 使用协程处理异步初始化
CoroutineScope(Dispatchers.IO).launch {
//获取语音识别的语种
val supportedLanguagesConfig = config["supportedLanguages"] as? List<String>
val supportedLanguages = supportedLanguagesConfig?.toTypedArray()
?: arrayOf("zh-CN", "en-US") // 默认支持多语言
val asrInitSuccess = initialize(
subscriptionKey = config["azureSpeechKey"]?.toString() ?: "",
region = config["azureSpeechRegion"]?.toString() ?: "",
supportedLanguages = supportedLanguages,
audioSourceType = if (isExternalActive) AzureAsrHelper.AudioSourceType.EXTERNAL
else AzureAsrHelper.AudioSourceType.MICROPHONE
)
@ -1091,6 +1103,58 @@ object AgentService : CoroutineScope {
}
}
/**
* 设置支持的语音识别语言
*/
fun setSupportedLanguages(languages: List<String>): Boolean {
if (!isInitialized) {
Log.e(TAG, "服务未初始化")
sendEvent("error", mapOf("code" to "NOT_INITIALIZED", "message" to "服务未初始化"))
return false
}
if (languages.isEmpty()) {
Log.e(TAG, "语言列表不能为空")
sendEvent("error", mapOf("code" to "EMPTY_LANGUAGES", "message" to "语言列表不能为空"))
return false
}
return try {
Log.i(TAG, "设置支持的语言: ${languages.joinToString(", ")}")
// 重新初始化ASR服务以应用新的语言设置
val success = azureAsrHelper?.initialize(
subscriptionKey = this.azureSpeechKey,
region = this.azureSpeechRegion,
supportedLanguages = languages.toTypedArray(),
audioSourceType = audioSourceType
) ?: false
if (success) {
Log.i(TAG, "语言设置更新成功")
sendEvent("languages_updated", mapOf(
"languages" to languages,
"status" to "success"
))
} else {
Log.e(TAG, "语言设置更新失败")
sendEvent("error", mapOf(
"code" to "LANGUAGE_UPDATE_FAILED",
"message" to "语言设置更新失败"
))
}
success
} catch (e: Exception) {
Log.e(TAG, "设置支持语言异常: ${e.message}")
sendEvent("error", mapOf(
"code" to "LANGUAGE_UPDATE_ERROR",
"message" to e.message.toString()
))
false
}
}
/**
* 保存聊天消息
*/

16
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt

@ -201,6 +201,22 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
}
}
"setSupportedLanguages" -> {
val languages = call.argument<List<String>>("languages")
if (languages.isNullOrEmpty()) {
result.error("INVALID_ARGUMENTS", "语言列表不能为空", null)
return
}
try {
val isSuccess = AgentService.setSupportedLanguages(languages)
result.success(isSuccess)
} catch (e: Exception) {
Log.e(TAG, "设置支持语言失败", e)
result.error("SET_LANGUAGES_ERROR", "设置支持语言失败: ${e.message}", null)
}
}
"stopAiSteam" -> {
try {
AgentService.stopAiStream()

56
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift

@ -27,6 +27,7 @@ class AgentServiceImpl: NSObject {
private var azureSpeechKey: String = ""
private var azureSpeechRegion: String = ""
private var config: [String: Any] = [:]
private var isSpeaking: Bool = false
@ -136,6 +137,8 @@ class AgentServiceImpl: NSObject {
if isInitialized { return true }
// os_log("initialize: config=%{public}@", log: logger, type: .info, config)
self.config = config
if let openaiApiKey = config["openaiApiKey"] as? String {
self.apiKey = openaiApiKey
} else {
@ -199,11 +202,14 @@ class AgentServiceImpl: NSObject {
private func initializeAzureSpeech() -> Bool {
azureTtsHelper?.addListener(self)
// 获取支持的语言列表,支持从配置中读取
let supportedLanguages = config["supportedLanguages"] as? [String] ?? ["zh-CN", "en-US"]
guard let asrSuccess = azureAsrHelper?.initialize(
subscriptionKey: azureSpeechKey,
region: azureSpeechRegion,
supportedLanguages: ["zh-CN"],
supportedLanguages: supportedLanguages,
audioSourceType: .microphone
), asrSuccess else {
sendError("初始化语音识别服务失败", code: "ASR_INIT_ERROR")
@ -844,6 +850,54 @@ private func processNavigationCard(_ cardNavigation: [String: Any]) {
return "通过test插件集成MacPaw OpenAI SDK v0.4.3 - 集成成功"
}
/**
* 设置支持的语音识别语言
*
* @param languages 支持的语言列表,如 ["zh-CN", "en-US"]
* @return 是否设置成功
*/
func setSupportedLanguages(_ languages: [String]) -> Bool {
os_log("设置支持的语言: %{public}@", log: logger, type: .info, languages.joined(separator: ", "))
if !isInitialized {
os_log("服务未初始化,无法设置语言", log: logger, type: .error)
sendError("服务未初始化", code: "NOT_INITIALIZED")
return false
}
if languages.isEmpty {
os_log("语言列表为空,使用默认语言", log: logger, type: .info)
return false
}
// 如果正在识别,需要先停止
let wasRecognizing = isRecognizing
if wasRecognizing {
_ = stopRecognition()
}
// 重新初始化ASR引擎,使用新的语言设置
guard let success = azureAsrHelper?.initialize(
subscriptionKey: azureSpeechKey,
region: azureSpeechRegion,
supportedLanguages: languages,
audioSourceType: .microphone
), success else {
os_log("重新初始化ASR引擎失败", log: logger, type: .error)
sendError("重新初始化ASR引擎失败", code: "ASR_INIT_ERROR")
return false
}
os_log("成功设置支持的语言: %{public}@", log: logger, type: .info, languages.joined(separator: ", "))
// 如果之前正在识别,重新开始
if wasRecognizing {
_ = startRecognition()
}
return true
}
func dispose() -> Bool {
print("ai释放资源")
if isRecognizing {

10
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift

@ -101,6 +101,16 @@ public class AgentServicePlugin: NSObject, FlutterPlugin {
let success = impl.dispose()
result(success)
case "setSupportedLanguages":
guard let arguments = call.arguments as? [String: Any],
let languages = arguments["languages"] as? [String] else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "语言列表不能为空", details: nil))
return
}
let success = impl.setSupportedLanguages(languages)
result(success)
default:
result(FlutterMethodNotImplemented)
}

15
local_plugins/agent_service/lib/agent_service.dart

@ -332,6 +332,21 @@ class AgentService {
}
}
/// 设置支持的语音识别语言
///
/// [languages] 支持的语言列表,如 ['zh-CN', 'en-US']
/// 返回是否设置成功
static Future<bool> setSupportedLanguages(List<String> languages) async {
try {
final bool result = await _channel.invokeMethod('setSupportedLanguages', {
'languages': languages,
});
return result;
} on PlatformException catch (e) {
throw AgentServiceException(e.code, e.message ?? '设置支持语言失败', e.details);
}
}
/// 停止语音合成
static Future<bool> stopAiSteam() async {
try {

4
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt

@ -87,7 +87,7 @@ class AzureAsrHelper(private val context: Context) {
fun initialize(
subscriptionKey: String,
region: String,
supportedLanguages: Array<String> = arrayOf("zh-CN"),
supportedLanguages: Array<String> = arrayOf("zh-CN", "en-US"),
audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE
): Boolean {
try {
@ -171,7 +171,7 @@ class AzureAsrHelper(private val context: Context) {
}
setupMicrophoneStream()
recognizer?.close()
recognizer?.close()
recognizer = null
//创建识别器
recognizer = if (isAutoDetectLanguage) {

83
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt

@ -106,6 +106,15 @@ object BleService {
// 初始化状态
private var isInitialized = false
private val replyTimeoutHandler = Handler(Looper.getMainLooper())
private val replyTimeoutRunnable = Runnable {
if (!isReply) {
Log.w(TAG, "命令响应超时,自动重置 isReply 为 true")
isReply = true
processNextCommand()
}
}
/**
* 初始化BLE服务
* @param appContext 应用上下文
@ -348,17 +357,34 @@ object BleService {
}
return null;
}
fun getDeviceInfos(deviceName: String): List<Map<String, Any>>? {
fun getDeviceInfos(deviceName: String): List<Map<String, Any>> {
// 更新缓存
val matchedResults = scanResults.filter {
it.device.name == deviceName
}
Log.d(TAG, "找到 ${matchedResults.size} 个名称为 $deviceName 的设备")
if (matchedResults.isEmpty()) {
return emptyList()
val ret = mutableListOf<Map<String, Any>>()
// 如果找到匹配设备名的结果,返回匹配设备的信息
if (matchedResults.isNotEmpty()) {
for (result in matchedResults) {
val info = printDeviceInfo(result)
if (info != null) {
ret.add(info)
}
}
} else {
// 如果没找到匹配设备名的结果,返回所有扫描到的设备信息
for (result in scanResults) {
val info = printDeviceInfo(result)
if (info != null) {
ret.add(info)
}
}
}
return null;
return ret
}
/**
* 打印设备信息和服务UUID
@ -386,7 +412,7 @@ object BleService {
// 解析服务数据
scanRecord.serviceData?.forEach { (uuid, data) ->
val dataHex = data.joinToString("") { String.format("%02X", it) }
Log.d(TAG, "服务数据: UUID=${uuid.uuid}, 数据=$dataHex")
//Log.d(TAG, "服务数据: UUID=${uuid.uuid}, 数据=$dataHex")
}
}
return info
@ -482,6 +508,8 @@ object BleService {
} catch (e: Exception) {
Log.e(TAG, "关闭GATT连接异常: ${e.message}", e)
}
isReply = true;
commandQueue.clear()
bluetoothGatt = null
notifyChar = null
writeChar = null
@ -531,6 +559,7 @@ object BleService {
newState == BluetoothProfile.STATE_DISCONNECTED -> {
updateConnectionState(BleConst.STATE_DISCONNECTED)
disconnectGatt()
// otaManager?.onBtDeviceConnection(g.device, StateCode.CONNECTION_CONNECTING)
// g.close()
// otaManager?.release();
@ -763,19 +792,10 @@ object BleService {
}
}"
)
isReply = true;
cmdReplyType = cmdType;
if (!commandQueue.isEmpty()) {
val nextCmd = commandQueue.poll()
writeChar!!.value = nextCmd
var isSeccess = bluetoothGatt?.writeCharacteristic(writeChar)
if (isSeccess == true) {
Log.d(
TAG,
"收到设备响应:"
)
}
}
isReply = true
cmdReplyType = cmdType
replyTimeoutHandler.removeCallbacks(replyTimeoutRunnable) // 移除超时回调
processNextCommand()
// 根据命令类型处理不同的响应
when (cmdType) {
BleConst.CMD_GET_VERSION -> {
@ -1396,13 +1416,16 @@ object BleService {
// 加入队列并触发发送
if (isReply) {
var isSeccess = bluetoothGatt?.writeCharacteristic(writeChar)
if (isSeccess == true) {
isReply = false;
cmdType;
val isSuccess = bluetoothGatt?.writeCharacteristic(writeChar)
if (isSuccess == true) {
isReply = false
replyTimeoutHandler.postDelayed(replyTimeoutRunnable, 1000) // 设置1秒超时
return true
} else
{
commandQueue.poll()
return false
}
} else {
commandQueue.offer(cmdPacket)
}
@ -1414,6 +1437,19 @@ object BleService {
}
}
private fun processNextCommand() {
if (!commandQueue.isEmpty()) {
val nextCmd = commandQueue.poll()
writeChar!!.value = nextCmd
val isSuccess = bluetoothGatt?.writeCharacteristic(writeChar)
if (isSuccess == true) {
isReply = false
replyTimeoutHandler.postDelayed(replyTimeoutRunnable, 1000) // 设置1秒超时
Log.d(TAG, "发送队列中的下一个命令")
}
}
}
/**
* 计算CRC8/MAXIM校验值
*/
@ -1599,4 +1635,3 @@ class RecordingFile(private val context: Context) {
}
}
}

4
local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleConst.swift

@ -25,10 +25,10 @@ class BleConst {
static let CLIENT_CHAR_CONFIG_UUID = CBUUID(string: "00002902-0000-1000-8000-00805f9b34fb")
/** 扫描服务UUID - 用于后台扫描过滤 */
static let SERVICE_UUID = CBUUID(string: "FD7B")
static let SERVICE_UUID = CBUUID(string: "abc0")
/** 扫描服务UUID1 - 用于后台扫描过滤 */
static let SERVICE_UUID1 = CBUUID(string: "FF11")
static let SERVICE_UUID1 = CBUUID(string: "fd7b")
/** BLE扫描Action */
static let ACTION_SCAN_RESULT = "com.yunqiinnovation.ble_service.ACTION_BLE_SCAN"

9
local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift

@ -233,11 +233,12 @@ private var cmdReplyType: UInt8 = 0
// manager.scanForPeripherals(withServices: nil, options: [
// CBCentralManagerScanOptionAllowDuplicatesKey: true // 允许重复以获取更新的广播数据
// ])
//let serviceUUIDs = [BleConst.SERVICE_UUID, BleConst.SERVICE_UUID1]
manager.scanForPeripherals(withServices: nil, options: [
CBCentralManagerScanOptionAllowDuplicatesKey: true
let serviceUUIDs = [BleConst.SERVICE_UUID, BleConst.SERVICE_UUID1]
manager.scanForPeripherals(withServices: serviceUUIDs, options: [
CBCentralManagerScanOptionAllowDuplicatesKey: true // 允许重复以获取更新的广播数据
])
os_log("🔍 开始扫描所有BLE设备(无过滤),超时时间: %.1f秒", log: logger, type: .info, timeout)
os_log("🔍 开始扫描所有BLE设备(无过滤),超时时间: %.1f秒", log: logger, type: .info, timeout)
// 设置扫描超时
scanTimer?.invalidate()

17
pubspec.yaml

@ -23,10 +23,9 @@ dependencies:
audio_session: ^0.1.18
flutter_markdown: ^0.7.7+1
markdown: ^7.1.1
appflowy_editor:
git:
url: https://github.com/AppFlowy-IO/appflowy-editor.git
ref: main
fleather: ^1.24.0+1
parchment: ^1.24.0
htmltopdfwidgets: ^1.1.0
wifi_iot: ^0.3.19+2
permission_handler: ^10.4.5
web_socket_channel: ^2.4.0
@ -68,7 +67,7 @@ dependencies:
flutter_facebook_auth: ^7.1.1
flutter_image_compress: ^2.4.0
common_utils: ^2.1.0
# image_cropper: ^9.0.0
image_gallery_saver_plus: ^4.0.1
dio: ^5.8.0+1
sqflite: ^2.4.2
path: ^1.9.1
@ -124,15 +123,19 @@ flutter:
shaders:
- shaders/mic_flow.frag
assets:
- assets/images/headphone_light.png
- assets/images/headphone_dark.png
# - assets/images/headphone_light.png
# - assets/images/headphone_dark.png
- assets/images/robot_light.png
- assets/images/glasses_light.png
- assets/images/agents/
- assets/images/explore.png
- assets/images/explore1.png
- assets/images/explore2.jpg
- assets/images/mine-background.png
- assets/fonts/NotoSansSC-Regular.ttf
- assets/lottie/
- assets/js/
- assets/images/headphone_new4.png
- assets/images/headphone_new3.png
- .env
- .mcp.json

Loading…
Cancel
Save