diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b547109..6edb925 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -5,6 +5,12 @@ + + + + + + + // 检查是否是 IM SDK 相关的异常 + val stackTrace = exception.stackTraceToString() + if (stackTrace.contains("easemob") || stackTrace.contains("im_flutter_sdk") || + stackTrace.contains("ClientWrapper")) { + // IM SDK 异常,只记录日志,不崩溃应用 + android.util.Log.e("MainActivity", "IM SDK 异常(已捕获,应用继续运行): ${exception.message}") + android.util.Log.e("MainActivity", "堆栈跟踪: $stackTrace") + // 不调用默认处理器,让应用继续运行 + return@setDefaultUncaughtExceptionHandler + } + + // 其他异常,使用默认处理器 + defaultExceptionHandler?.uncaughtException(thread, exception) + } + } +} diff --git a/assets/images/im_coin_icon.png b/assets/images/im_coin_icon.png new file mode 100644 index 0000000..d8c4d7b Binary files /dev/null and b/assets/images/im_coin_icon.png differ diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index ec2e698..6de6103 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -42,7 +42,11 @@ NSPhotoLibraryUsageDescription - Replace with your permission description. + 需要访问相册以选择和发送图片、视频 + NSCameraUsageDescription + 需要访问相机以拍摄照片和视频 + NSMicrophoneUsageDescription + 需要访问麦克风以录制语音和视频 UIApplicationSupportsIndirectInputEvents UILaunchStoryboardName diff --git a/lib/controller/message/conversation_controller.dart b/lib/controller/message/conversation_controller.dart index 8c01d61..9e6cdf3 100644 --- a/lib/controller/message/conversation_controller.dart +++ b/lib/controller/message/conversation_controller.dart @@ -2,6 +2,7 @@ import 'package:get/get.dart'; import 'package:im_flutter_sdk/im_flutter_sdk.dart'; import '../../im/im_manager.dart'; import '../../model/mine/user_base_data.dart'; +import '../mine/user_controller.dart'; import 'chat_controller.dart'; // 扩展类用于存储用户信息(包括业务系统的信息) @@ -291,7 +292,71 @@ class ConversationController extends GetxController { /// 刷新会话列表 Future refreshConversations() async { - await loadConversations(); + // 如果IM未登录,先尝试登录 + if (!IMManager.instance.isLoggedIn) { + if (Get.isLogEnable) { + Get.log('🔄 [ConversationController] IM未登录,尝试重新登录...'); + } + + try { + // 尝试获取token并登录 + if (Get.isRegistered()) { + final userController = Get.find(); + final token = await userController.getHxUserToken(); + + if (token != null) { + // 等待登录完成(最多等待5秒) + int waitCount = 0; + const maxWait = 10; // 最多等待10次,每次500ms,总共5秒 + + while (waitCount < maxWait && !IMManager.instance.isLoggedIn) { + await Future.delayed(Duration(milliseconds: 500)); + waitCount++; + } + + if (IMManager.instance.isLoggedIn) { + if (Get.isLogEnable) { + Get.log('✅ [ConversationController] IM登录成功,开始加载会话列表'); + } + // 登录成功后加载会话列表 + await loadConversations(); + return; + } else { + if (Get.isLogEnable) { + Get.log('⚠️ [ConversationController] IM登录超时'); + } + errorMessage.value = 'IM登录超时,请稍后重试'; + isLoading.value = false; + return; + } + } else { + if (Get.isLogEnable) { + Get.log('❌ [ConversationController] 获取IM token失败'); + } + errorMessage.value = '获取IM token失败,请稍后重试'; + isLoading.value = false; + return; + } + } else { + if (Get.isLogEnable) { + Get.log('❌ [ConversationController] UserController未注册'); + } + errorMessage.value = 'IM未登录,请稍后重试'; + isLoading.value = false; + return; + } + } catch (e) { + if (Get.isLogEnable) { + Get.log('❌ [ConversationController] 重试登录失败: $e'); + } + errorMessage.value = '重试登录失败,请稍后重试'; + isLoading.value = false; + return; + } + } else { + // 如果已登录,直接加载会话列表 + await loadConversations(); + } } /// 清除会话列表和用户信息缓存(用于退出登录时) diff --git a/lib/controller/mine/user_controller.dart b/lib/controller/mine/user_controller.dart index 52efd57..72c2ca2 100644 --- a/lib/controller/mine/user_controller.dart +++ b/lib/controller/mine/user_controller.dart @@ -37,7 +37,13 @@ class UserController extends GetxController { if (token != null) { // 打印获取的token print('获取环信用户token成功: $token'); - IMManager.instance.login(token); + // 等待登录完成 + final loginSuccess = await IMManager.instance.login(token); + if (loginSuccess) { + print('✅ IM登录成功'); + } else { + print('⚠️ IM登录失败'); + } return token; } else { SmartDialog.showToast('获取的环信用户token为空'); diff --git a/lib/generated/assets.dart b/lib/generated/assets.dart index 71d5e02..ee639aa 100644 --- a/lib/generated/assets.dart +++ b/lib/generated/assets.dart @@ -221,5 +221,5 @@ class Assets { static const String imagesWallet = 'assets/images/wallet.png'; static const String imagesWechatPay = 'assets/images/wechat_pay.png'; static const String imagesWomenIcon = 'assets/images/women_icon.png'; - + static const String imagesImCoinIcon = 'assets/images/im_coin_icon.png'; } diff --git a/lib/im/im_manager.dart b/lib/im/im_manager.dart index e6bf471..1b44a1a 100644 --- a/lib/im/im_manager.dart +++ b/lib/im/im_manager.dart @@ -1,4 +1,7 @@ import 'dart:io'; +import 'dart:async'; +import 'package:flutter/widgets.dart'; +import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:im_flutter_sdk/im_flutter_sdk.dart'; @@ -9,6 +12,7 @@ import '../controller/message/conversation_controller.dart'; import '../controller/message/chat_controller.dart'; import '../controller/global.dart'; import '../pages/mine/login_page.dart'; +import '../network/user_api.dart'; // 完整的IM管理器实现,使用实际的SDK类型和方法 class IMManager { @@ -19,9 +23,11 @@ class IMManager { // 静态getter用于instance访问 static IMManager get instance => _instance; - bool _isInitialized = false; + bool _initialized = false; bool _listenersRegistered = false; - bool _isLoggedIn = false; + bool _loggedIn = false; + bool _isReconnecting = false; // 是否正在重连 + Completer? _initCompleter; // 用于确保并发调用时只初始化一次 // 监听器标识符 static const String _connectionHandlerKey = 'im_manager_connection_handler'; @@ -38,31 +44,89 @@ class IMManager { print('IMManager instance created'); } - /// 初始化IM SDK - Future initialize(String appKey) async { + bool get isInitialized => _initialized; + bool get isLoggedIn => _loggedIn; + + /// 确保 IM 初始化完成(支持并发调用,只初始化一次) + Future ensureInitialized({required String appKey}) async { + // 如果已经初始化,直接返回 + if (_initialized) { + debugPrint('✅ IM 已初始化,无需重复初始化'); + return; + } + + // 如果正在初始化,等待完成 + if (_initCompleter != null) { + debugPrint('🟡 IM 初始化中,等待完成...'); + return _initCompleter!.future; + } + + _initCompleter = Completer(); + + debugPrint('🟡 IM 开始初始化'); + debugPrint('📋 AppKey: $appKey'); + try { - if (_isInitialized) { - print('IM SDK already initialized'); - return true; + // 确保 Flutter 绑定已初始化 + WidgetsFlutterBinding.ensureInitialized(); + debugPrint('✅ WidgetsFlutterBinding 已初始化'); + + // 验证 appKey 格式 + if (appKey.isEmpty) { + throw Exception('AppKey 不能为空'); + } + if (!appKey.contains('#')) { + debugPrint('⚠️ AppKey 格式可能不正确,应为 "orgname#appname" 格式'); } - // 创建EMOptions实例 - final options = EMOptions( - appKey: appKey, + // 创建 EMOptions + final options = EMOptions.withAppKey( + appKey, autoLogin: false, + debugMode: true, + usingHttpsOnly: true, acceptInvitationAlways: false, - debugMode: true ); + debugPrint('✅ EMOptions 创建成功'); + - // 初始化SDK - await EMClient.getInstance.init(options); + // 调用 SDK 初始化 + debugPrint('🟡 调用 EMClient.getInstance.init()...'); + try { + await EMClient.getInstance.init(options); + debugPrint('🟢 EMClient.getInstance.init() 调用完成'); + } on PlatformException catch (e) { + debugPrint('❌ PlatformException 捕获:'); + debugPrint(' code: ${e.code}'); + debugPrint(' message: ${e.message}'); + debugPrint(' details: ${e.details}'); + debugPrint(' stacktrace: ${e.stacktrace}'); + rethrow; + } catch (e, s) { + debugPrint('❌ 其他异常: $e'); + debugPrint('堆栈: $s'); + rethrow; + } - _isInitialized = true; - print('IM SDK initialized successfully'); - return true; - } catch (e) { - print('Failed to initialize IM SDK: $e'); - return false; + // 注册监听器 + _registerListeners(); + _initialized = true; + + debugPrint('✅ IM 初始化成功'); + _initCompleter!.complete(); + } catch (e, s) { + debugPrint('❌ IM 初始化失败'); + debugPrint('错误类型: ${e.runtimeType}'); + debugPrint('错误信息: $e'); + debugPrint('堆栈跟踪:'); + debugPrint('$s'); + + _initialized = false; + _initCompleter!.completeError(e, s); + _initCompleter = null; // 允许重试 + + // 重新抛出异常,让调用者知道初始化失败 + rethrow; } } @@ -82,17 +146,17 @@ class IMManager { _connectionHandlerKey, EMConnectionEventHandler( onConnected: () { - if (Get.isLogEnable) { - Get.log('✅ [IMManager] 已连接到IM服务器'); - } + // 重置重连状态 + _isReconnecting = false; + debugPrint('🔌 IM 已连接'); // 连接成功后,通知 ConversationController 加载会话列表 _refreshConversationList(); }, onDisconnected: () { - if (Get.isLogEnable) { - Get.log('❌ [IMManager] 与IM服务器断开连接'); - } - // TODO: 可以在这里添加自动重连逻辑 + debugPrint('🔌 IM 已断开'); + _loggedIn = false; + // 自动重连逻辑 + _handleDisconnected(); }, onTokenDidExpire: () { if (Get.isLogEnable) { @@ -117,9 +181,7 @@ class IMManager { _chatHandlerKey, EMChatEventHandler( onMessagesReceived: (messages) { - if (Get.isLogEnable) { - Get.log('📨 [IMManager] 收到 ${messages.length} 条新消息'); - } + debugPrint('📩 收到消息数: ${messages.length}'); // 从消息扩展字段中解析用户信息并缓存 for (var message in messages) { if (message.direction == MessageDirection.RECEIVE) { @@ -132,9 +194,64 @@ class IMManager { _notifyChatControllers(messages); }, onMessageContentChanged: (EMMessage message, String operatorId, int operationTime){ + Get.log('-------------📨 [IMManager] -------------: ${operatorId}'); + Get.log('-------------📨 [IMManager] -------------: ${message}'); + Get.log('-------------📨 [IMManager] -------------: ${operationTime}'); + Get.log('-------------📨 [IMManager] -------------: ${message.localTime}'); if (Get.isLogEnable) { Get.log('📨 [IMManager] 消息内容已修改: ${message.localTime}'); } + + // 处理金币数值:只对接收到的消息处理金币标签 + // 注意:根据实际API,金币数值可能通过 operatorId 或消息 attributes 传递 + // 只处理接收到的消息(自己发送的消息不显示金币标签) + if (message.direction == MessageDirection.RECEIVE) { + try { + double? coinValue; + + // 方法1:尝试从消息 attributes 中获取金币数值 + if (message.attributes != null) { + final coinValueStr = message.attributes!['coin_value'] as String?; + if (coinValueStr != null && coinValueStr.isNotEmpty) { + coinValue = double.tryParse(coinValueStr); + } + } + + // 方法2:如果 attributes 中没有,尝试从 operatorId 解析(如果它是数值) + if (coinValue == null && operatorId.isNotEmpty) { + coinValue = double.tryParse(operatorId); + } + + // 如果获取到金币数值,确保存储到消息的 attributes 中 + if (coinValue != null && coinValue > 0) { + if (message.attributes == null) { + message.attributes = {}; + } + message.attributes!['coin_value'] = coinValue.toString(); + + // 通知对应的 ChatController 更新消息 + final fromId = message.from; + if (fromId != null && fromId.isNotEmpty) { + final controller = _activeChatControllers[fromId]; + if (controller != null) { + // 更新消息列表中的消息 + final index = controller.messages.indexWhere((msg) => msg.msgId == message.msgId); + if (index != -1) { + controller.messages[index] = message; + controller.update(); + if (Get.isLogEnable) { + Get.log('✅ [IMManager] 已更新接收消息的金币数值: msgId=${message.msgId}, coinValue=$coinValue'); + } + } + } + } + } + } catch (e) { + if (Get.isLogEnable) { + Get.log('⚠️ [IMManager] 处理金币数值失败: $e'); + } + } + } } ), ); @@ -249,7 +366,8 @@ class IMManager { final userId = presence.publisher; if (userId != null && userId.isNotEmpty) { // 使用 statusDescription 字段来判断在线状态 - final statusDesc = presence.statusDescription?.toLowerCase() ?? ''; + final statusDescStr = presence.statusDescription ?? ''; + final statusDesc = statusDescStr.toLowerCase(); // 判断在线状态:online、available 等表示在线 final isOnline = statusDesc == 'online' || statusDesc == 'available'; @@ -292,54 +410,157 @@ class IMManager { } } - /// 登录IM服务 - Future login(String token) async { + /// 登录(真正判断 IM 是否可用的地方) + Future loginWithToken({ + required String appKey, + required String userId, + required String token, + }) async { try { - if (!_isInitialized) { - print('IM SDK not initialized'); - return false; - } - var userId = storage.read('userId'); - if (Get.isLogEnable) { - Get.log('🔐 [IMManager] 准备登录IM,userId: $userId'); - } else { - print('🔐 [IMManager] 准备登录IM,userId: $userId'); - } - await EMClient.getInstance.logout(); + await ensureInitialized(appKey: appKey); + + // 防止脏状态 + await EMClient.getInstance.logout().catchError((_) {}); + await EMClient.getInstance.loginWithToken(userId, token); - // 注册监听器 - _registerListeners(); - // 设置登录状态 - _isLoggedIn = true; + _loggedIn = true; + + // 重置重连状态 + _isReconnecting = false; // 登录成功后,通知 ConversationController 刷新会话列表 _refreshConversationList(); - if (Get.isLogEnable) { - Get.log('✅ [IMManager] IM登录成功,userId: $userId'); - } else { - print('✅ [IMManager] IM登录成功,userId: $userId'); - } + + debugPrint('✅ IM 登录成功: $userId'); return true; - } catch (e) { - if (Get.isLogEnable) { - Get.log('❌ [IMManager] IM登录失败: $e'); - } else { - print('❌ [IMManager] IM登录失败: $e'); - } + } catch (e, s) { + _loggedIn = false; + debugPrint('❌ IM 登录失败: $e'); + debugPrint('$s'); + return false; + } + } + + /// 登录IM服务(向后兼容方法) + Future login(String token) async { + final userId = storage.read('userId'); + if (userId == null) { + debugPrint('❌ IM 登录失败: userId 为空'); return false; } + return await loginWithToken( + appKey: '1165251016193374#demo', + userId: userId, + token: token, + ); } /// 登出IM服务 - Future logout() async { + Future logout() async { try { await EMClient.getInstance.logout(); - // 清除登录状态 - _isLoggedIn = false; - print('IM logout successful'); - return true; - } catch (e) { - print('IM logout failed: $e'); - return false; + } finally { + _loggedIn = false; + } + } + + /// 处理连接断开,自动重连 + void _handleDisconnected() async { + // 如果正在重连,避免重复重连 + if (_isReconnecting) { + if (Get.isLogEnable) { + Get.log('⚠️ [IMManager] 正在重连中,跳过重复重连'); + } + return; + } + + // 如果未初始化或未登录,不需要重连 + if (!_initialized || !_loggedIn) { + if (Get.isLogEnable) { + Get.log('⚠️ [IMManager] SDK未初始化或用户未登录,跳过重连'); + } + return; + } + + _isReconnecting = true; + + if (Get.isLogEnable) { + Get.log('🔄 [IMManager] 开始自动重连...'); + } else { + print('🔄 [IMManager] 开始自动重连...'); + } + + try { + // 延迟2秒后重连,避免频繁重连 + await Future.delayed(const Duration(seconds: 2)); + + // 检查是否仍然需要重连(可能在延迟期间已经连接成功) + if (!_loggedIn) { + if (Get.isLogEnable) { + Get.log('⚠️ [IMManager] 用户已登出,取消重连'); + } + _isReconnecting = false; + return; + } + + // 获取 UserApi 来获取新的 token + if (!Get.isRegistered()) { + if (Get.isLogEnable) { + Get.log('❌ [IMManager] UserApi 未注册,无法获取token重连'); + } + _isReconnecting = false; + return; + } + + final userApi = Get.find(); + final response = await userApi.getHxUserToken(); + + // 检查响应:code == 0 表示成功 + if (Get.isLogEnable) { + Get.log('📡 [IMManager] 获取token响应: code=${response.data.code}, message=${response.data.message}'); + } else { + print('📡 [IMManager] 获取token响应: code=${response.data.code}, message=${response.data.message}'); + } + + if (response.data.isSuccess && response.data.data != null) { + final token = response.data.data!; + if (Get.isLogEnable) { + Get.log('✅ [IMManager] 获取到新的token (长度: ${token.length}),开始重新登录'); + } else { + print('✅ [IMManager] 获取到新的token (长度: ${token.length}),开始重新登录'); + } + + // 重新登录 + final loginSuccess = await login(token); + if (loginSuccess) { + if (Get.isLogEnable) { + Get.log('✅ [IMManager] 自动重连成功'); + } else { + print('✅ [IMManager] 自动重连成功'); + } + } else { + if (Get.isLogEnable) { + Get.log('❌ [IMManager] 自动重连失败:登录失败'); + } else { + print('❌ [IMManager] 自动重连失败:登录失败'); + } + } + } else { + if (Get.isLogEnable) { + Get.log('❌ [IMManager] 获取token失败:code=${response.data.code}, message=${response.data.message}, data=${response.data.data}'); + } else { + print('❌ [IMManager] 获取token失败:code=${response.data.code}, message=${response.data.message}, data=${response.data.data}'); + } + } + } catch (e, stackTrace) { + if (Get.isLogEnable) { + Get.log('❌ [IMManager] 自动重连异常: $e'); + Get.log('堆栈跟踪: $stackTrace'); + } else { + print('❌ [IMManager] 自动重连异常: $e'); + print('堆栈跟踪: $stackTrace'); + } + } finally { + _isReconnecting = false; } } @@ -382,9 +603,6 @@ class IMManager { } } - /// 检查是否已登录 - bool get isLoggedIn => _isLoggedIn; - /// 检查用户是否存在于IM系统中 Future checkUserExists(String userId) async { try { @@ -676,7 +894,7 @@ class IMManager { /// [callback] 状态变化回调函数 Future subscribeUserPresence(String userId, Function(bool isOnline) callback) async { try { - if (!_isLoggedIn) { + if (!_loggedIn) { if (Get.isLogEnable) { Get.log('⚠️ [IMManager] IM未登录,无法订阅在线状态'); } @@ -730,7 +948,7 @@ class IMManager { /// [userId] 用户ID Future unsubscribeUserPresence(String userId) async { try { - if (!_isLoggedIn) { + if (!_loggedIn) { return false; } @@ -758,7 +976,7 @@ class IMManager { /// 返回 true 表示在线,false 表示离线,null 表示获取失败 Future getUserPresenceStatus(String userId) async { try { - if (!_isLoggedIn) { + if (!_loggedIn) { if (Get.isLogEnable) { Get.log('⚠️ [IMManager] IM未登录,无法获取在线状态'); } @@ -781,7 +999,8 @@ class IMManager { final presence = presences.first; // 使用 statusDescription 字段来判断在线状态 - final statusDesc = presence.statusDescription?.toLowerCase() ?? ''; + final statusDescStr = presence.statusDescription ?? ''; + final statusDesc = statusDescStr.toLowerCase(); // 判断在线状态:online、available 等表示在线 final isOnline = statusDesc == 'online' || statusDesc == 'available'; diff --git a/lib/main.dart b/lib/main.dart index 8cb1fb8..0026c02 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,6 +11,7 @@ import 'package:dating_touchme_app/rtc/rtc_manager.dart'; import 'package:dating_touchme_app/widget/live/draggable_overlay_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -19,39 +20,72 @@ import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; void main() async { - WidgetsFlutterBinding.ensureInitialized(); - // 初始化GetStorage - await GetStorage.init(); - - // 设置环境配置 - 根据是否为release模式 - EnvConfig.setEnvironment(Environment.dev); - await RTCManager.instance.initialize(appId: '4c2ea9dcb4c5440593a418df0fdd512d'); - await IMManager.instance.initialize('1165251016193374#demo'); - // 初始化全局依赖 - final networkService = NetworkService(); - Get.put(networkService); - Get.put(networkService.userApi); - Get.put(networkService.homeApi); - // 初始化全局 Overlay 控制器 - Get.put(OverlayController()); - - SystemChrome.setSystemUIOverlayStyle( - const SystemUiOverlayStyle( - statusBarColor: Colors.transparent, - statusBarIconBrightness: Brightness.dark, - statusBarBrightness: Brightness.light, - systemNavigationBarColor: Colors.white, - systemNavigationBarIconBrightness: Brightness.dark, - ), - ); - SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); - - if (Platform.isIOS) { - SystemChrome.setEnabledSystemUIMode( - SystemUiMode.manual, - overlays: [SystemUiOverlay.top], + // 设置全局错误处理 + FlutterError.onError = (FlutterErrorDetails details) { + FlutterError.presentError(details); + if (kReleaseMode) { + // 在生产环境中,可以将错误发送到错误收集服务 + print('Flutter Error: ${details.exception}'); + } + }; + + // 处理异步错误 + PlatformDispatcher.instance.onError = (error, stack) { + print('Platform Error: $error'); + print('Stack: $stack'); + return true; + }; + + try { + WidgetsFlutterBinding.ensureInitialized(); + // 初始化GetStorage + await GetStorage.init(); + + // 设置环境配置 - 根据是否为release模式 + EnvConfig.setEnvironment(Environment.dev); + + // 初始化RTC,如果失败也不阻止应用启动 + try { + await RTCManager.instance.initialize(appId: '4c2ea9dcb4c5440593a418df0fdd512d'); + } catch (e) { + print('RTC初始化失败: $e'); + } + + + // 注意:IM初始化改为异步,在应用启动后执行,避免阻塞应用启动 + // IM初始化将在 MyApp 的 initState 中异步执行 + + // 初始化全局依赖 + final networkService = NetworkService(); + Get.put(networkService); + Get.put(networkService.userApi); + Get.put(networkService.homeApi); + // 初始化全局 Overlay 控制器 + Get.put(OverlayController()); + + SystemChrome.setSystemUIOverlayStyle( + const SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: Brightness.dark, + statusBarBrightness: Brightness.light, + systemNavigationBarColor: Colors.white, + systemNavigationBarIconBrightness: Brightness.dark, + ), ); + SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); + + if (Platform.isIOS) { + SystemChrome.setEnabledSystemUIMode( + SystemUiMode.manual, + overlays: [SystemUiOverlay.top], + ); + } + } catch (e, stackTrace) { + print('应用初始化失败: $e'); + print('堆栈跟踪: $stackTrace'); + // 即使初始化失败,也尝试启动应用 } + runApp( GetMaterialApp( locale: Get.locale, // 使用GetX的locale @@ -82,16 +116,23 @@ void main() async { child ?? const SizedBox(), // 全局 overlay 组件 Obx(() { - final overlayController = Get.find(); - return overlayController.showOverlay.value - ? DraggableOverlayWidget( - size: 60, - backgroundColor: const Color.fromRGBO(0, 0, 0, 0.6), - onClose: () { - overlayController.hide(); - }, - ) - : const SizedBox.shrink(); + try { + if (Get.isRegistered()) { + final overlayController = Get.find(); + return overlayController.showOverlay.value + ? DraggableOverlayWidget( + size: 60, + backgroundColor: const Color.fromRGBO(0, 0, 0, 0.6), + onClose: () { + overlayController.hide(); + }, + ) + : const SizedBox.shrink(); + } + } catch (e) { + print('获取OverlayController失败: $e'); + } + return const SizedBox.shrink(); }), ], ), @@ -110,29 +151,72 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { - Fluwx fluwx = Fluwx(); + bool _screenUtilInitialized = false; @override void initState() { super.initState(); _initFluwx(); + // 异步初始化IM,避免阻塞应用启动 + _initIMAsync(); + } + + /// 异步初始化IM SDK,避免阻塞应用启动 + void _initIMAsync() { + // 使用 Future.delayed 确保在应用完全启动后再初始化 + Future.delayed(const Duration(milliseconds: 500), () async { + try { + print('🚀 开始异步初始化IM SDK(懒加载模式)...'); + print('📋 AppKey: 1165251016193374#demo'); + + // 使用 ensureInitialized 方法,确保 init 至少被调用过一次 + await IMManager.instance.ensureInitialized(appKey: '1165251016193374#demo'); + + print('✅ IM SDK异步初始化完成'); + } catch (e, stackTrace) { + print('❌ IM SDK异步初始化失败'); + print('错误类型: ${e.runtimeType}'); + print('错误信息: $e'); + print('堆栈跟踪:'); + print('$stackTrace'); + + // 检查是否是特定错误 + if (e.toString().contains('PlatformException')) { + print('⚠️ 可能是原生层配置问题,请检查:'); + print(' 1. Android: 检查 AndroidManifest.xml 中的权限配置'); + print(' 2. iOS: 检查 Info.plist 中的权限配置'); + print(' 3. 检查网络连接'); + } + + // IM初始化失败不应该阻止应用运行 + } + }); } _initFluwx() async { - await fluwx.registerApi( - appId: 'wx57624b8918fdd95c', - doOnAndroid: true, - doOnIOS: true, - universalLink: 'https://your.univerallink.com/link/', - ); - await fluwx.isWeChatInstalled; + try { + await fluwx.registerApi( + appId: 'wx57624b8918fdd95c', + doOnAndroid: true, + doOnIOS: true, + universalLink: 'https://your.univerallink.com/link/', + ); + await fluwx.isWeChatInstalled; + } catch (e) { + print('微信SDK初始化失败: $e'); + // 微信SDK初始化失败不应该阻止应用运行 + } } // This widget is the root of your application. @override Widget build(BuildContext context) { - ScreenUtil.init(context, designSize: const Size(375, 812)); + // ScreenUtil.init 只应该初始化一次,不应该在每次build时调用 + if (!_screenUtilInitialized) { + ScreenUtil.init(context, designSize: const Size(375, 812)); + _screenUtilInitialized = true; + } // 判断token是否为空 final storage = GetStorage(); diff --git a/lib/model/home/marriage_data.dart b/lib/model/home/marriage_data.dart index 7f3e683..6a8acf9 100644 --- a/lib/model/home/marriage_data.dart +++ b/lib/model/home/marriage_data.dart @@ -49,8 +49,8 @@ class MarriageData { factory MarriageData.fromJson(Map json) { return MarriageData( - miId: '1180578682436194304',//json['miId'] ?? '', - userId: '1114267797208305664',//json['userId'] ?? '', + miId: json['miId'] ?? '', + userId: json['userId'] ?? '', profilePhoto: json['profilePhoto'] ?? '', nickName: json['nickName'] ?? '', isRealNameCertified: json['isRealNameCertified'] ?? false, diff --git a/lib/widget/message/image_item.dart b/lib/widget/message/image_item.dart index c696ad4..d08ccad 100644 --- a/lib/widget/message/image_item.dart +++ b/lib/widget/message/image_item.dart @@ -232,6 +232,9 @@ class _ImageItemState extends State { @override Widget build(BuildContext context) { + // 检查是否有金币数值(只对接收的消息显示) + final coinValue = _getCoinValue(); + return Column( children: [ if (widget.showTime) _buildTimeLabel(), @@ -264,20 +267,31 @@ class _ImageItemState extends State { ), if (widget.isSentByMe) SizedBox(width: 10.w), - // 图片容器 - GestureDetector( - onTap: _onImageTap, - child: Container( - margin: EdgeInsets.only(top: 10.h), - decoration: BoxDecoration( - color: widget.isSentByMe ? Color(0xff8E7BF6) : Colors.white, - borderRadius: BorderRadius.circular(18.w), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(18.w), - child: _buildImageContent(width, height), + Column( + crossAxisAlignment: widget.isSentByMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, + children: [ + // 图片容器 + GestureDetector( + onTap: _onImageTap, + child: Container( + margin: EdgeInsets.only(top: 10.h), + decoration: BoxDecoration( + color: widget.isSentByMe ? Color(0xff8E7BF6) : Colors.white, + borderRadius: BorderRadius.circular(18.w), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(18.w), + child: _buildImageContent(width, height), + ), + ), ), - ), + // 金币标签(只对接收的消息显示) + if (!widget.isSentByMe && coinValue != null) + Padding( + padding: EdgeInsets.only(top: 10.h), + child: _buildCoinLabel(coinValue), + ), + ], ), if (widget.isSentByMe) SizedBox(width: 8.w), @@ -637,6 +651,51 @@ class _ImageItemState extends State { } } + // 获取金币数值 + double? _getCoinValue() { + try { + final attributes = widget.message.attributes; + if (attributes != null && attributes.containsKey('coin_value')) { + final coinValueStr = attributes['coin_value'] as String?; + if (coinValueStr != null && coinValueStr.isNotEmpty) { + return double.tryParse(coinValueStr); + } + } + } catch (e) { + // 忽略错误 + } + return null; + } + + // 构建金币标签 + Widget _buildCoinLabel(double coinValue) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), + decoration: BoxDecoration( + color: Color.fromRGBO(0, 0, 0, 0.05), + borderRadius: BorderRadius.circular(20.w), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + Assets.imagesImCoinIcon, + width: 16.w, + height: 16.w, + ), + SizedBox(width: 4.w), + Text( + '+${coinValue.toStringAsFixed(2)}', + style: TextStyle( + fontSize: 12.sp, + color: Color.fromRGBO(255, 132, 0, 1), + ), + ), + ], + ), + ); + } + // 点击图片事件 void _onImageTap() { // 检查是否有可显示的图片 diff --git a/lib/widget/message/text_item.dart b/lib/widget/message/text_item.dart index 77694a1..769cc01 100644 --- a/lib/widget/message/text_item.dart +++ b/lib/widget/message/text_item.dart @@ -25,6 +25,9 @@ class TextItem extends StatelessWidget { @override Widget build(BuildContext context) { + // 检查是否有金币数值(只对接收的消息显示) + final coinValue = _getCoinValue(); + return Column( children: [ // 显示时间 @@ -50,30 +53,41 @@ class TextItem extends StatelessWidget { ), ), if (isSentByMe) SizedBox(width: 10.w), - Container( - constraints: BoxConstraints(maxWidth: 240.w), - margin: EdgeInsets.only(top: 10.h), - padding: EdgeInsets.symmetric( - horizontal: 12.w, - vertical: 8.h, - ), - decoration: BoxDecoration( - color: isSentByMe ? Color(0xff8E7BF6) : Colors.white, // 发送方紫色,接收方白色 - borderRadius: BorderRadius.only( - topLeft: isSentByMe ? Radius.circular(12.w) : Radius.circular(0), - topRight: isSentByMe ? Radius.circular(0) : Radius.circular(12.w), - bottomLeft: Radius.circular(12.w), - bottomRight: Radius.circular(12.w), - ), - ), - child: EmojiTextWidget( - text: textBody.content, - textStyle: TextStyle( - fontSize: 14.sp, - color: isSentByMe ? Colors.white : Colors.black, // 发送方白色文字,接收方黑色文字 + Column( + crossAxisAlignment: isSentByMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, + children: [ + Container( + constraints: BoxConstraints(maxWidth: 240.w), + margin: EdgeInsets.only(top: 10.h), + padding: EdgeInsets.symmetric( + horizontal: 12.w, + vertical: 8.h, + ), + decoration: BoxDecoration( + color: isSentByMe ? Color(0xff8E7BF6) : Colors.white, // 发送方紫色,接收方白色 + borderRadius: BorderRadius.only( + topLeft: isSentByMe ? Radius.circular(12.w) : Radius.circular(0), + topRight: isSentByMe ? Radius.circular(0) : Radius.circular(12.w), + bottomLeft: Radius.circular(12.w), + bottomRight: Radius.circular(12.w), + ), + ), + child: EmojiTextWidget( + text: textBody.content, + textStyle: TextStyle( + fontSize: 14.sp, + color: isSentByMe ? Colors.white : Colors.black, // 发送方白色文字,接收方黑色文字 + ), + emojiSize: 24.w, + ), ), - emojiSize: 24.w, - ), + // 金币标签(只对接收的消息显示) + if (!isSentByMe && coinValue != null) + Padding( + padding: EdgeInsets.only(top: 10.h), + child: _buildCoinLabel(coinValue), + ), + ], ), if (isSentByMe) SizedBox(width: 8.w), if (isSentByMe) _buildAvatar(), @@ -121,6 +135,51 @@ class TextItem extends StatelessWidget { ); } + // 获取金币数值 + double? _getCoinValue() { + try { + final attributes = message.attributes; + if (attributes != null && attributes.containsKey('coin_value')) { + final coinValueStr = attributes['coin_value'] as String?; + if (coinValueStr != null && coinValueStr.isNotEmpty) { + return double.tryParse(coinValueStr); + } + } + } catch (e) { + // 忽略错误 + } + return null; + } + + // 构建金币标签 + Widget _buildCoinLabel(double coinValue) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), + decoration: BoxDecoration( + color: Color.fromRGBO(0, 0, 0, 0.05), + borderRadius: BorderRadius.circular(20.w), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + Assets.imagesImCoinIcon, + width: 16.w, + height: 16.w, + ), + SizedBox(width: 4.w), + Text( + '+${coinValue.toStringAsFixed(2)}', + style: TextStyle( + fontSize: 12.sp, + color: Color.fromRGBO(255, 132, 0, 1), + ), + ), + ], + ), + ); + } + // 构建消息状态(发送中、已发送、失败重发) Widget _buildMessageStatus() { // 只对发送的消息显示状态 diff --git a/lib/widget/message/video_item.dart b/lib/widget/message/video_item.dart index a10e880..ae4e8de 100644 --- a/lib/widget/message/video_item.dart +++ b/lib/widget/message/video_item.dart @@ -203,6 +203,9 @@ class _VideoItemState extends State { @override Widget build(BuildContext context) { + // 检查是否有金币数值(只对接收的消息显示) + final coinValue = _getCoinValue(); + return Column( children: [ if (widget.showTime) _buildTimeLabel(), @@ -233,142 +236,153 @@ class _VideoItemState extends State { ), ), if (widget.isSentByMe) SizedBox(width: 10.w), - // 🚀 极致性能优化:无需初始化,直接显示缩略图 - Stack( + Column( + crossAxisAlignment: widget.isSentByMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ - GestureDetector( - onTap: _playVideo, - child: Container( - margin: EdgeInsets.only(top: 10.h), - width: 180.w, - height: 180.w * (304 / 289), // 按289:304比例计算高度 - decoration: BoxDecoration( - color: Colors.grey[300], - borderRadius: BorderRadius.circular(18.w), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 8, - offset: Offset(0, 2), - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(18.w), - child: Stack( - fit: StackFit.expand, - children: [ - // 🚀 背景层:默认占位符 - Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - Colors.grey[300]!, - Colors.grey[400]!, - ], - ), - ), - child: Icon( - Icons.videocam, - size: 48.w, - color: Colors.grey[600], - ), - ), - // 🚀 缩略图层:始终显示缩略图(如果有) - if (_thumbnailPath != null && _thumbnailPath!.isNotEmpty) - _buildThumbnail(), - // 播放按钮和时长覆盖层 - Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Colors.black.withOpacity(0.1), - Colors.black.withOpacity(0.5), - ], - ), + // 🚀 极致性能优化:无需初始化,直接显示缩略图 + Stack( + children: [ + GestureDetector( + onTap: _playVideo, + child: Container( + margin: EdgeInsets.only(top: 10.h), + width: 180.w, + height: 180.w * (304 / 289), // 按289:304比例计算高度 + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(18.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + offset: Offset(0, 2), ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // 播放按钮(增强动画效果) - Container( - padding: EdgeInsets.all(8.w), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - shape: BoxShape.circle, - ), - child: Icon( - Icons.play_arrow_rounded, - size: 48.w, - color: Colors.white, + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(18.w), + child: Stack( + fit: StackFit.expand, + children: [ + // 🚀 背景层:默认占位符 + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.grey[300]!, + Colors.grey[400]!, + ], ), ), - ], - ), - ), - // 右下角显示时长标签 - Positioned( - right: 8.w, - bottom: 8.h, - child: Container( - padding: EdgeInsets.symmetric( - horizontal: 8.w, - vertical: 4.h, - ), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.7), - borderRadius: BorderRadius.circular(4.w), + child: Icon( + Icons.videocam, + size: 48.w, + color: Colors.grey[600], + ), ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.videocam_rounded, - size: 12.w, - color: Colors.white, + // 🚀 缩略图层:始终显示缩略图(如果有) + if (_thumbnailPath != null && _thumbnailPath!.isNotEmpty) + _buildThumbnail(), + // 播放按钮和时长覆盖层 + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.black.withOpacity(0.1), + Colors.black.withOpacity(0.5), + ], ), - SizedBox(width: 4.w), - Text( - _formatDuration(widget.videoBody.duration ?? 0), - style: TextStyle( - fontSize: 11.sp, - color: Colors.white, - fontWeight: FontWeight.w600, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // 播放按钮(增强动画效果) + Container( + padding: EdgeInsets.all(8.w), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + shape: BoxShape.circle, + ), + child: Icon( + Icons.play_arrow_rounded, + size: 48.w, + color: Colors.white, + ), ), + ], + ), + ), + // 右下角显示时长标签 + Positioned( + right: 8.w, + bottom: 8.h, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 8.w, + vertical: 4.h, + ), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.7), + borderRadius: BorderRadius.circular(4.w), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.videocam_rounded, + size: 12.w, + color: Colors.white, + ), + SizedBox(width: 4.w), + Text( + _formatDuration(widget.videoBody.duration ?? 0), + style: TextStyle( + fontSize: 11.sp, + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + ], ), - ], + ), ), - ), + ], ), - ], + ), ), ), - ), - ), - // 🚀 加载指示器(播放时显示) - if (_isLoadingVideo) - Positioned.fill( - child: Container( - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.5), - borderRadius: BorderRadius.circular(18.w), - ), - child: Center( - child: CircularProgressIndicator( - strokeWidth: 3, - valueColor: AlwaysStoppedAnimation( - Colors.white, + // 🚀 加载指示器(播放时显示) + if (_isLoadingVideo) + Positioned.fill( + child: Container( + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), + borderRadius: BorderRadius.circular(18.w), + ), + child: Center( + child: CircularProgressIndicator( + strokeWidth: 3, + valueColor: AlwaysStoppedAnimation( + Colors.white, + ), + ), ), ), ), - ), - ), - ], - ), + ], + ), + // 金币标签(只对接收的消息显示) + if (!widget.isSentByMe && coinValue != null) + Padding( + padding: EdgeInsets.only(top: 10.h), + child: _buildCoinLabel(coinValue), + ), + ], + ), if (widget.isSentByMe) SizedBox(width: 8.w), if (widget.isSentByMe) _buildAvatar(), ], @@ -445,6 +459,51 @@ class _VideoItemState extends State { } } + // 获取金币数值 + double? _getCoinValue() { + try { + final attributes = widget.message.attributes; + if (attributes != null && attributes.containsKey('coin_value')) { + final coinValueStr = attributes['coin_value'] as String?; + if (coinValueStr != null && coinValueStr.isNotEmpty) { + return double.tryParse(coinValueStr); + } + } + } catch (e) { + // 忽略错误 + } + return null; + } + + // 构建金币标签 + Widget _buildCoinLabel(double coinValue) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), + decoration: BoxDecoration( + color: Color.fromRGBO(0, 0, 0, 0.05), + borderRadius: BorderRadius.circular(20.w), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + Assets.imagesImCoinIcon, + width: 16.w, + height: 16.w, + ), + SizedBox(width: 4.w), + Text( + '+${coinValue.toStringAsFixed(2)}', + style: TextStyle( + fontSize: 12.sp, + color: Color.fromRGBO(255, 132, 0, 1), + ), + ), + ], + ), + ); + } + // 🚀 性能优化:构建缓存的缩略图 Widget _buildThumbnail() { if (_thumbnailPath == null || _thumbnailPath!.isEmpty) { diff --git a/lib/widget/message/voice_item.dart b/lib/widget/message/voice_item.dart index 3c0a001..cb3bc82 100644 --- a/lib/widget/message/voice_item.dart +++ b/lib/widget/message/voice_item.dart @@ -234,6 +234,9 @@ class _VoiceItemState extends State with TickerProviderStateMixin { // 判断当前音频是否正在播放 final isPlaying = _playerManager.isPlaying(widget.messageId); + + // 检查是否有金币数值(只对接收的消息显示) + final coinValue = _getCoinValue(); return Column( children: [ @@ -249,59 +252,70 @@ class _VoiceItemState extends State with TickerProviderStateMixin { children: [ if (!widget.isSentByMe) _buildAvatar(), if (!widget.isSentByMe) SizedBox(width: 8.w), - Container( - margin: EdgeInsets.only(top: 10.h), - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.h), - decoration: BoxDecoration( - color: widget.isSentByMe ? Color(0xff8E7BF6) : Colors.white, - borderRadius: BorderRadius.only( - topLeft: widget.isSentByMe - ? Radius.circular(12.w) - : Radius.circular(0), - topRight: widget.isSentByMe - ? Radius.circular(0) - : Radius.circular(12.w), - bottomLeft: Radius.circular(12.w), - bottomRight: Radius.circular(12.w), - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - // 播放按钮 - Container( - width: 20.w, - height: 20.w, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: widget.isSentByMe ? Colors.white : Colors.black, - ), - child: Icon( - isPlaying ? Icons.pause : Icons.play_arrow, - color: widget.isSentByMe - ? Color(0xff8E7BF6) - : Colors.white, - size: 16.w, + Column( + crossAxisAlignment: widget.isSentByMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, + children: [ + Container( + margin: EdgeInsets.only(top: 10.h), + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.h), + decoration: BoxDecoration( + color: widget.isSentByMe ? Color(0xff8E7BF6) : Colors.white, + borderRadius: BorderRadius.only( + topLeft: widget.isSentByMe + ? Radius.circular(12.w) + : Radius.circular(0), + topRight: widget.isSentByMe + ? Radius.circular(0) + : Radius.circular(12.w), + bottomLeft: Radius.circular(12.w), + bottomRight: Radius.circular(12.w), ), ), - SizedBox(width: 8.w), - // 时长文本 - Text( - durationText, - style: TextStyle( - fontSize: 14.sp, - color: widget.isSentByMe ? Colors.white : Colors.black, - fontWeight: FontWeight.w500, - ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + // 播放按钮 + Container( + width: 20.w, + height: 20.w, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: widget.isSentByMe ? Colors.white : Colors.black, + ), + child: Icon( + isPlaying ? Icons.pause : Icons.play_arrow, + color: widget.isSentByMe + ? Color(0xff8E7BF6) + : Colors.white, + size: 16.w, + ), + ), + SizedBox(width: 8.w), + // 时长文本 + Text( + durationText, + style: TextStyle( + fontSize: 14.sp, + color: widget.isSentByMe ? Colors.white : Colors.black, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(width: 8.w), + // 音频波形 + _buildWaveform(), + ], + ), + ).onTap(() { + _handlePlayPause(); + }), + // 金币标签(只对接收的消息显示) + if (!widget.isSentByMe && coinValue != null) + Padding( + padding: EdgeInsets.only(top: 10.h), + child: _buildCoinLabel(coinValue), ), - SizedBox(width: 8.w), - // 音频波形 - _buildWaveform(), - ], - ), - ).onTap(() { - _handlePlayPause(); - }), + ], + ), if (widget.isSentByMe) SizedBox(width: 8.w), if (widget.isSentByMe) _buildAvatar(), ], @@ -341,6 +355,53 @@ class _VoiceItemState extends State with TickerProviderStateMixin { ); } + // 获取金币数值 + double? _getCoinValue() { + try { + if (widget.message != null) { + final attributes = widget.message!.attributes; + if (attributes != null && attributes.containsKey('coin_value')) { + final coinValueStr = attributes['coin_value'] as String?; + if (coinValueStr != null && coinValueStr.isNotEmpty) { + return double.tryParse(coinValueStr); + } + } + } + } catch (e) { + // 忽略错误 + } + return null; + } + + // 构建金币标签 + Widget _buildCoinLabel(double coinValue) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), + decoration: BoxDecoration( + color: Color.fromRGBO(0, 0, 0, 0.05), + borderRadius: BorderRadius.circular(20.w), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + Assets.imagesImCoinIcon, + width: 16.w, + height: 16.w, + ), + SizedBox(width: 4.w), + Text( + '+${coinValue.toStringAsFixed(2)}', + style: TextStyle( + fontSize: 12.sp, + color: Color.fromRGBO(255, 132, 0, 1), + ), + ), + ], + ), + ); + } + // 构建音频波形 Widget _buildWaveform() { // 根据时长生成波形条数量(最多20个) diff --git a/location_plugin/example/pubspec.lock b/location_plugin/example/pubspec.lock index 678892a..36a3835 100644 --- a/location_plugin/example/pubspec.lock +++ b/location_plugin/example/pubspec.lock @@ -6,7 +6,7 @@ packages: description: name: async sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.13.0" boolean_selector: @@ -14,7 +14,7 @@ packages: description: name: boolean_selector sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" characters: @@ -22,7 +22,7 @@ packages: description: name: characters sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.0" clock: @@ -30,7 +30,7 @@ packages: description: name: clock sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.2" collection: @@ -38,7 +38,7 @@ packages: description: name: collection sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.19.1" cupertino_icons: @@ -46,7 +46,7 @@ packages: description: name: cupertino_icons sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.8" fake_async: @@ -54,7 +54,7 @@ packages: description: name: fake_async sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.3" file: @@ -62,7 +62,7 @@ packages: description: name: file sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "7.0.1" flutter: @@ -80,7 +80,7 @@ packages: description: name: flutter_lints sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.0.0" flutter_test: @@ -98,7 +98,7 @@ packages: description: name: http sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.6.0" http_parser: @@ -106,7 +106,7 @@ packages: description: name: http_parser sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.1.2" integration_test: @@ -119,7 +119,7 @@ packages: description: name: leak_tracker sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "11.0.2" leak_tracker_flutter_testing: @@ -127,7 +127,7 @@ packages: description: name: leak_tracker_flutter_testing sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.10" leak_tracker_testing: @@ -135,7 +135,7 @@ packages: description: name: leak_tracker_testing sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.2" lints: @@ -143,7 +143,7 @@ packages: description: name: lints sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.1.1" location_plugin: @@ -158,7 +158,7 @@ packages: description: name: matcher sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.12.17" material_color_utilities: @@ -166,7 +166,7 @@ packages: description: name: material_color_utilities sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.11.1" meta: @@ -174,7 +174,7 @@ packages: description: name: meta sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.16.0" path: @@ -182,7 +182,7 @@ packages: description: name: path sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.9.1" platform: @@ -190,7 +190,7 @@ packages: description: name: platform sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.6" plugin_platform_interface: @@ -198,7 +198,7 @@ packages: description: name: plugin_platform_interface sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.8" process: @@ -206,7 +206,7 @@ packages: description: name: process sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.0.5" sky_engine: @@ -219,7 +219,7 @@ packages: description: name: source_span sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.10.1" stack_trace: @@ -227,7 +227,7 @@ packages: description: name: stack_trace sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.12.1" stream_channel: @@ -235,7 +235,7 @@ packages: description: name: stream_channel sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.4" string_scanner: @@ -243,7 +243,7 @@ packages: description: name: string_scanner sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.1" sync_http: @@ -251,7 +251,7 @@ packages: description: name: sync_http sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.1" term_glyph: @@ -259,7 +259,7 @@ packages: description: name: term_glyph sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.2" test_api: @@ -267,7 +267,7 @@ packages: description: name: test_api sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.7.6" typed_data: @@ -275,7 +275,7 @@ packages: description: name: typed_data sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.0" vector_math: @@ -283,7 +283,7 @@ packages: description: name: vector_math sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" vm_service: @@ -291,7 +291,7 @@ packages: description: name: vm_service sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "15.0.2" web: @@ -299,7 +299,7 @@ packages: description: name: web sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.1" webdriver: @@ -307,7 +307,7 @@ packages: description: name: webdriver sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0" sdks: