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: